Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4857ace
feat(giftcard): add giftcard_website junction + 1.0.0→1.1.0 backfill
Jun 25, 2026
cd406b8
feat(giftcard): persist website_ids via junction, validate per-website
Jun 25, 2026
f747d2b
feat(giftcard): admin form multiselect + edit-mode reassign
Jun 25, 2026
913cc54
fix(giftcard): admin grid renders + filters website membership
Jun 25, 2026
a8ad1b8
feat(giftcard): admin edit page transaction-history tab
Jun 25, 2026
d2b4db6
feat(giftcard): customer "Check Balance" My Account page
Jun 25, 2026
f53e3cd
chore(giftcard): display admin balance fields to 2dp
Jun 25, 2026
b9f71f3
feat(giftcard): rate-limit failed customer balance lookups
Jun 25, 2026
b715233
chore(giftcard): add en_US locale strings for new features
Jun 25, 2026
0bdc2a6
chore(giftcard): satisfy PHPStan + house style on new tab/controller …
Jun 25, 2026
9c031d3
fix(giftcard): preserve legacy website_id fallback in getWebsiteIds()
Jun 25, 2026
3702e0d
fix(giftcard): route customer balance flash errors via customer/session
Jun 25, 2026
0bb4af1
lint
fballiano Jun 25, 2026
e36a83d
fix(giftcard): address review findings on multi-website + balance page
fballiano Jun 25, 2026
9d4eaac
fix(adminhtml): stop calling deprecated getSaveUrl() in form containe…
fballiano Jun 25, 2026
7ecfa20
chore(phpstan): drop stale getSaveUrl() deprecation baseline entry
fballiano Jun 25, 2026
f504848
refactor(giftcard): move junction backfill to data setup phase
fballiano Jun 25, 2026
866b007
Merge branch 'main' into feat/giftcard-multi-website-and-customer-bal…
fballiano Jun 25, 2026
a8a7aba
Migrated gift cards fully to the giftcard_website junction, dropping …
fballiano Jul 15, 2026
838b055
Removed redundant migration comment from giftcard schema.php
fballiano Jul 15, 2026
4bfe2e8
Merge remote-tracking branch 'origin/main' into feat/giftcard-multi-w…
fballiano Jul 15, 2026
8acf006
Merge branch 'main' into feat/giftcard-multi-website-and-customer-bal…
fballiano Jul 24, 2026
f04b6b0
Merge branch 'main' into feat/giftcard-multi-website-and-customer-bal…
fballiano Jul 25, 2026
0e89e4d
Merge branch 'main' into feat/giftcard-multi-website-and-customer-bal…
fballiano Jul 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/code/core/Mage/Checkout/controllers/CartController.php
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@ protected function _tryApplyGiftcard(string $code): array

// Check website validity
$websiteId = (int) $quote->getStore()->getWebsiteId();
if ((int) $giftcard->getWebsiteId() !== $websiteId) {
if (!in_array($websiteId, $giftcard->getWebsiteIds(), true)) {
return ['success' => false, 'message' => ''];
}

Expand Down
20 changes: 19 additions & 1 deletion app/code/core/Maho/Giftcard/Api/GiftCard.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@
'senderName' => ['type' => 'String', 'description' => 'Sender name'],
'senderEmail' => ['type' => 'String', 'description' => 'Sender email'],
'message' => ['type' => 'String', 'description' => 'Gift card message'],
'websiteId' => ['type' => 'Int', 'description' => 'Website ID (defaults to current)'],
'websiteIds' => ['type' => '[Int!]', 'description' => 'Website IDs the card is valid on (defaults to current website)'],
'expiresAt' => ['type' => 'String', 'description' => 'Expiration date (YYYY-MM-DD)'],
],
description: 'Create a new gift card',
Expand Down Expand Up @@ -120,4 +120,22 @@ class GiftCard extends CrudResource
public ?string $senderName = null;
public ?string $senderEmail = null;
public ?string $message = null;

/**
* Websites the card is valid on (giftcard_website junction). Maps to the
* model's `website_ids` pending-change key, so applyToModel() feeds the
* junction sync; omitting it on create falls back to the current website.
*
* @var int[]|null
*/
public ?array $websiteIds = null;

public static function afterLoad(self $dto, object $model): void
{
// The junction is lazy-loaded, never present in the model's data
// array, so fromModel()'s convention mapping leaves this null.
if ($model instanceof \Maho_Giftcard_Model_Giftcard && $model->getId()) {
$dto->websiteIds = $model->getWebsiteIds();
}
}
}
4 changes: 3 additions & 1 deletion app/code/core/Maho/Giftcard/Api/GiftCardProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,9 +95,11 @@ private function createGiftcardFromGraphQl(array $context): GiftCard
'sender_name' => $args['senderName'] ?? null,
'sender_email' => $args['senderEmail'] ?? null,
'message' => $args['message'] ?? null,
'website_id' => isset($args['websiteId']) ? (int) $args['websiteId'] : null,
'expires_at' => $args['expiresAt'] ?? null,
]);
if (!empty($args['websiteIds'])) {
$giftcard->setWebsiteIds(array_map('intval', (array) $args['websiteIds']));
}
$giftcard->save();
$this->sendEmailToRecipient($giftcard);

Expand Down
264 changes: 13 additions & 251 deletions app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Form.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,266 +8,28 @@

declare(strict_types=1);

/**
* Empty `<form id="edit_form">` wrapper auto-resolved by the Edit
* Form_Container as its `form` child. The actual fields live in the
* Edit/Tab/Form.php block (registered as the General tab in the
* adminhtml_giftcard_edit layout handle) and are injected into this
* wrapper by the Mage tabs JavaScript via the `destElementId` hook.
*
* Mirrors the canonical Mage_Adminhtml_Block_Cms_Page_Edit_Form pattern.
*/
class Maho_Giftcard_Block_Adminhtml_Giftcard_Edit_Form extends Mage_Adminhtml_Block_Widget_Form
{
#[\Override]
protected function _prepareForm()
{
$model = Mage::registry('current_giftcard');

$form = new Maho\Data\Form([
'id' => 'edit_form',
'action' => $this->getUrl('*/*/save', ['id' => $this->getRequest()->getParam('id')]),
'method' => 'post',
$form = new \Maho\Data\Form([
'id' => 'edit_form',
'action' => $this->getUrl('*/*/save', ['id' => $this->getRequest()->getParam('id')]),
'method' => 'post',
'enctype' => 'multipart/form-data',
]);

$form->setUseContainer(true);

$fieldset = $form->addFieldset('base_fieldset', ['legend' => Mage::helper('giftcard')->__('Gift Card Information')]);

if ($model->getId()) {
$fieldset->addField('giftcard_id', 'hidden', [
'name' => 'giftcard_id',
]);
}

$fieldset->addField('code', 'text', [
'name' => 'code',
'label' => Mage::helper('giftcard')->__('Code'),
'title' => Mage::helper('giftcard')->__('Code'),
'required' => false,
'note' => 'Leave empty to auto-generate',
'disabled' => $model->getId() ? true : false,
]);

$fieldset->addField('status', 'select', [
'label' => Mage::helper('giftcard')->__('Status'),
'title' => Mage::helper('giftcard')->__('Status'),
'name' => 'status',
'required' => true,
'options' => [
Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE => 'Active',
Maho_Giftcard_Model_Giftcard::STATUS_DISABLED => 'Disabled',
Maho_Giftcard_Model_Giftcard::STATUS_USED => 'Used',
Maho_Giftcard_Model_Giftcard::STATUS_EXPIRED => 'Expired',
],
]);

// Website selector with currency mapping
$websites = Mage::app()->getWebsites();
$websiteCurrencies = [];
$websiteValues = [];
foreach ($websites as $website) {
$websiteValues[$website->getId()] = $website->getName();
$websiteCurrencies[$website->getId()] = $website->getBaseCurrencyCode();
}

if (!$model->getId()) {
$defaultWebsiteId = $model->getWebsiteId() ?: (int) array_key_first($websiteCurrencies);
$defaultCurrency = $websiteCurrencies[$defaultWebsiteId] ?? '';

$fieldset->addField('website_id', 'select', [
'name' => 'website_id',
'label' => Mage::helper('giftcard')->__('Website'),
'title' => Mage::helper('giftcard')->__('Website'),
'required' => true,
'values' => $websiteValues,
'value' => $defaultWebsiteId,
'after_element_html' => $this->_getWebsiteCurrencyScript($websiteCurrencies),
]);
$currencyNote = '<span class="giftcard-currency-note">[' . $defaultCurrency . ']</span>';
} else {
// Show website as read-only for existing gift cards
$website = Mage::app()->getWebsite($model->getWebsiteId());
$fieldset->addField('website_display', 'note', [
'label' => Mage::helper('giftcard')->__('Website'),
'text' => $website->getName() . ' (Base Currency: ' . $model->getCurrencyCode() . ')',
]);
$fieldset->addField('website_id', 'hidden', [
'name' => 'website_id',
]);
$currencyNote = '[' . $model->getCurrencyCode() . ']';
}

if (!$model->getId()) {
$fieldset->addField('balance', 'text', [
'name' => 'balance',
'label' => Mage::helper('giftcard')->__('Amount'),
'title' => Mage::helper('giftcard')->__('Amount'),
'required' => true,
'class' => 'validate-number validate-greater-than-zero',
'note' => $currencyNote,
]);

// Hidden field to sync initial_balance with balance on create
$fieldset->addField('initial_balance', 'hidden', [
'name' => 'initial_balance',
]);
} else {
// Existing gift card - show initial balance as read-only reference
$website = Mage::app()->getWebsite($model->getWebsiteId());
$formattedInitialBalance = $website->getBaseCurrency()->formatPrecision(
$model->getInitialBalance(),
2,
[],
false,
);

$fieldset->addField('initial_balance_display', 'note', [
'label' => Mage::helper('giftcard')->__('Initial Balance'),
'text' => $formattedInitialBalance,
]);

// Current balance is editable for manual adjustments
$fieldset->addField('balance', 'text', [
'name' => 'balance',
'label' => Mage::helper('giftcard')->__('Current Balance'),
'title' => Mage::helper('giftcard')->__('Current Balance'),
'required' => true,
'class' => 'validate-number',
'note' => $currencyNote . '<br>' . Mage::helper('giftcard')->__('Edit this to manually adjust the balance. Use "Admin Comment" to explain the adjustment.'),
]);
}

$fieldset->addField('expires_at', 'date', [
'name' => 'expires_at',
'label' => Mage::helper('giftcard')->__('Expires At'),
'title' => Mage::helper('giftcard')->__('Expires At'),
'image' => $this->getSkinUrl('images/grid-cal.gif'),
'format' => 'yyyy-MM-dd',
'note' => 'Leave empty for no expiration',
]);

$fieldset->addField('recipient_name', 'text', [
'name' => 'recipient_name',
'label' => Mage::helper('giftcard')->__('Recipient Name'),
'title' => Mage::helper('giftcard')->__('Recipient Name'),
]);

$fieldset->addField('recipient_email', 'text', [
'name' => 'recipient_email',
'label' => Mage::helper('giftcard')->__('Recipient Email'),
'title' => Mage::helper('giftcard')->__('Recipient Email'),
'class' => 'validate-email',
]);

$fieldset->addField('sender_name', 'text', [
'name' => 'sender_name',
'label' => Mage::helper('giftcard')->__('Sender Name'),
'title' => Mage::helper('giftcard')->__('Sender Name'),
]);

$fieldset->addField('sender_email', 'text', [
'name' => 'sender_email',
'label' => Mage::helper('giftcard')->__('Sender Email'),
'title' => Mage::helper('giftcard')->__('Sender Email'),
'class' => 'validate-email',
]);

$fieldset->addField('message', 'textarea', [
'name' => 'message',
'label' => Mage::helper('giftcard')->__('Message'),
'title' => Mage::helper('giftcard')->__('Message'),
]);

$fieldset->addField('comment', 'textarea', [
'name' => 'comment',
'label' => Mage::helper('giftcard')->__('Admin Comment'),
'title' => Mage::helper('giftcard')->__('Admin Comment'),
'note' => 'For admin records (balance adjustments)',
]);

// Show QR code and barcode for existing gift cards
if ($model->getId()) {
$helper = Mage::helper('giftcard');

$fieldset->addField('qr_barcode_display', 'note', [
'label' => Mage::helper('giftcard')->__('QR Code & Barcode'),
'text' => $this->_getQrBarcodeHtml($model, $helper),
]);
}

$form->setValues($model->getData());
$this->setForm($form);

return parent::_prepareForm();
}

/**
* Get JavaScript for updating currency display when website changes
*
* @param array<int, string> $websiteCurrencies
*/
protected function _getWebsiteCurrencyScript(array $websiteCurrencies): string
{
$currenciesJson = Mage::helper('core')->jsonEncode($websiteCurrencies);

return <<<HTML
<script>
(function() {
const websiteCurrencies = {$currenciesJson};

function updateCurrencyDisplay() {
const websiteSelect = document.getElementById('website_id');
if (!websiteSelect) return;

const websiteId = websiteSelect.value;
const currency = websiteCurrencies[websiteId] || '';
const currencyText = '[' + currency + ']';

document.querySelectorAll('.giftcard-currency-note').forEach(function(el) {
el.textContent = currencyText;
});
}

function init() {
const websiteSelect = document.getElementById('website_id');
if (websiteSelect) {
websiteSelect.addEventListener('change', updateCurrencyDisplay);
}
}

if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
</script>
HTML;
}

/**
* Get QR code and barcode HTML
*
* @param Maho_Giftcard_Model_Giftcard $model
* @param Maho_Giftcard_Helper_Data $helper
*/
protected function _getQrBarcodeHtml($model, $helper): string
{
$html = '<div style="display: flex; gap: 20px; align-items: flex-start;">';

if ($helper->isQrCodeEnabled()) {
$qrUrl = $helper->getQrCodeDataUrl($model->getCode(), 200);
$html .= '<div style="text-align: center;">';
$html .= '<div style="margin-bottom: 5px;"><strong>QR Code (Scannable)</strong></div>';
$html .= '<img src="' . htmlspecialchars($qrUrl) . '" alt="QR Code" style="border: 1px solid #ccc; padding: 5px; background: white;">';
$html .= '<div style="margin-top: 5px; font-family: monospace;">' . htmlspecialchars($model->getCode()) . '</div>';
$html .= '</div>';
}

if ($helper->isBarcodeEnabled()) {
$barcodeUrl = $helper->getBarcodeDataUrl($model->getCode());
$html .= '<div style="text-align: center;">';
$html .= '<div style="margin-bottom: 5px;"><strong>Barcode (Code128)</strong></div>';
$html .= '<img src="' . htmlspecialchars($barcodeUrl) . '" alt="Barcode" style="border: 1px solid #ccc; padding: 5px; background: white; max-width: 300px;">';
$html .= '<div style="margin-top: 5px; font-family: monospace;">' . htmlspecialchars($model->getCode()) . '</div>';
$html .= '</div>';
}

$html .= '</div>';

return $html;
}
}
Loading
Loading