diff --git a/app/code/core/Mage/Checkout/controllers/CartController.php b/app/code/core/Mage/Checkout/controllers/CartController.php index 49854d85a3..d9626e666c 100644 --- a/app/code/core/Mage/Checkout/controllers/CartController.php +++ b/app/code/core/Mage/Checkout/controllers/CartController.php @@ -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' => '']; } diff --git a/app/code/core/Maho/ApiPlatform/symfony/Trait/StoreRestrictionTrait.php b/app/code/core/Maho/ApiPlatform/symfony/Trait/StoreRestrictionTrait.php index 805aced992..cb88d6c98a 100644 --- a/app/code/core/Maho/ApiPlatform/symfony/Trait/StoreRestrictionTrait.php +++ b/app/code/core/Maho/ApiPlatform/symfony/Trait/StoreRestrictionTrait.php @@ -84,6 +84,45 @@ protected function assertWebsiteAllowed(int|string|null $websiteId, ApiUser $use } } + /** + * Read gate for an entity associated with a set of websites: deny when none + * of them is within the user's scope. An empty set is denied, matching the + * single-website rule. + * + * @param int[] $entityWebsiteIds + */ + protected function assertAnyWebsiteAllowed(array $entityWebsiteIds, ApiUser $user, string $entityLabel): void + { + $websiteIds = $this->allowedWebsiteIds($user); + if ($websiteIds === null) { + return; + } + + if (array_intersect(array_map(intval(...), $entityWebsiteIds), $websiteIds) === []) { + throw new AccessDeniedHttpException("Access denied for this {$entityLabel}'s website"); + } + } + + /** + * Write gate for an entity associated with a set of websites: deny unless + * every one of them is within the user's scope, so a restricted token + * cannot edit a record that also reaches a website it cannot see. + * + * @param int[] $entityWebsiteIds + */ + protected function assertAllWebsitesAllowed(array $entityWebsiteIds, ApiUser $user, string $entityLabel): void + { + $websiteIds = $this->allowedWebsiteIds($user); + if ($websiteIds === null) { + return; + } + + $entityWebsiteIds = array_map(intval(...), $entityWebsiteIds); + if ($entityWebsiteIds === [] || array_diff($entityWebsiteIds, $websiteIds) !== []) { + throw new AccessDeniedHttpException("Access denied for this {$entityLabel}'s website"); + } + } + /** * Map a restricted user's allowed store ids to their website ids; null for * unrestricted users, meaning "no website restriction". An allowlisted diff --git a/app/code/core/Maho/Giftcard/Api/GiftCard.php b/app/code/core/Maho/Giftcard/Api/GiftCard.php index edc9362350..214bd8f4ec 100644 --- a/app/code/core/Maho/Giftcard/Api/GiftCard.php +++ b/app/code/core/Maho/Giftcard/Api/GiftCard.php @@ -75,7 +75,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', @@ -115,7 +115,6 @@ class GiftCard extends CrudResource public ?string $status = null; public ?string $expiresAt = null; - public ?int $websiteId = null; #[ApiProperty(writable: false)] public ?string $currencyCode = null; @@ -144,6 +143,15 @@ class GiftCard extends CrudResource 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; + /** Not persisted on the card: recorded as the giftcard_history comment of a balance adjustment. */ #[ApiProperty(readable: false)] public ?string $comment = null; @@ -161,6 +169,12 @@ public static function afterLoad(self $dto, object $model): void { $dto->currencyCode = $model->getCurrencyCode(); + // 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(); + } + foreach ($model->getHistoryCollection() as $entry) { $dto->history[] = [ 'action' => $entry->getData('action'), diff --git a/app/code/core/Maho/Giftcard/Api/GiftCardProcessor.php b/app/code/core/Maho/Giftcard/Api/GiftCardProcessor.php index 2607180a01..a5431fd8c3 100644 --- a/app/code/core/Maho/Giftcard/Api/GiftCardProcessor.php +++ b/app/code/core/Maho/Giftcard/Api/GiftCardProcessor.php @@ -46,22 +46,26 @@ protected function afterSave(object $model, CrudResource $data): void } /** - * REST create path: default an omitted websiteId to the current store's - * website (a NULL-website card is unredeemable everywhere), then enforce - * the token's website scope on the resolved value. + * REST create path: default omitted websiteIds to the current store's + * website (a card with no website is unredeemable everywhere), then enforce + * the token's website scope on the resolved set. */ #[\Override] protected function beforeSave(object $model, CrudResource $data, ApiUser $user): void { - if (!$model->getId() && !$model->getData('website_id')) { - $model->setData('website_id', (int) StoreContext::getStore()->getWebsiteId()); + if (!$model->getId() && $model->getData('website_ids') === null) { + $model->setWebsiteIds([(int) StoreContext::getStore()->getWebsiteId()]); } if ($model->getId()) { // The card as stored must be in scope too, or a restricted token - // could claim a foreign card by rewriting its websiteId. - $this->assertWebsiteAllowed($model->getOrigData('website_id'), $user, 'gift card'); + // could claim a foreign card by rewriting its websiteIds. + $this->assertAllWebsitesAllowed( + $model->getResource()->getWebsiteIds((int) $model->getId()), + $user, + 'gift card', + ); } - $this->assertWebsiteAllowed((int) $model->getData('website_id'), $user, 'gift card'); + $this->assertAllWebsitesAllowed($model->getWebsiteIds(), $user, 'gift card'); } /** Balance bounds and duplicate-code check for the REST CRUD create/update path. */ @@ -85,12 +89,17 @@ protected function validate(CrudResource $data, object $model, bool $isNew): voi $this->assertBalanceBounds((float) $data->balance); } $this->assertValidStatus($data->status); - if ($data->websiteId !== null) { - try { - \Mage::app()->getWebsite($data->websiteId); - } catch (\Throwable) { - throw new BadRequestHttpException("Unknown website id {$data->websiteId}"); - } + foreach ($data->websiteIds ?? [] as $websiteId) { + $this->assertKnownWebsite((int) $websiteId); + } + } + + private function assertKnownWebsite(int $websiteId): void + { + try { + \Mage::app()->getWebsite($websiteId); + } catch (\Throwable) { + throw new BadRequestHttpException("Unknown website id {$websiteId}"); } } @@ -118,7 +127,7 @@ protected function processUpdate(int $id, mixed $data, ApiUser $user): mixed /** @var \Maho_Giftcard_Model_Giftcard $model */ $model = $this->loadOrFail($this->modelAlias, $id, 'Gift card not found'); - $this->assertWebsiteAllowed($model->getWebsiteId(), $user, 'gift card'); + $this->assertAllWebsitesAllowed($model->getWebsiteIds(), $user, 'gift card'); $oldData = $model->getData(); $this->assertValidStatus($data->status); @@ -186,18 +195,16 @@ private function createGiftcardFromGraphQl(array $context): GiftCard null, ); - // Default an omitted websiteId to the current store's website (the - // documented behavior; a NULL-website card is unredeemable everywhere) - // and enforce the token's website scope on the resolved value. - $websiteId = isset($args['websiteId']) - ? (int) $args['websiteId'] - : (int) StoreContext::getStore()->getWebsiteId(); - try { - \Mage::app()->getWebsite($websiteId); - } catch (\Throwable) { - throw new BadRequestHttpException("Unknown website id {$websiteId}"); + // Default omitted websiteIds to the current store's website (the + // documented behavior; a card with no website is unredeemable + // everywhere) and enforce the token's website scope on the resolved set. + $websiteIds = empty($args['websiteIds']) + ? [(int) StoreContext::getStore()->getWebsiteId()] + : array_map(intval(...), (array) $args['websiteIds']); + foreach ($websiteIds as $websiteId) { + $this->assertKnownWebsite($websiteId); } - $this->assertWebsiteAllowed($websiteId, $this->requireUser(), 'gift card'); + $this->assertAllWebsitesAllowed($websiteIds, $this->requireUser(), 'gift card'); $giftcard = \Mage::getModel('giftcard/giftcard'); $giftcard->setData([ @@ -209,9 +216,9 @@ private function createGiftcardFromGraphQl(array $context): GiftCard 'sender_name' => $args['senderName'] ?? null, 'sender_email' => $args['senderEmail'] ?? null, 'message' => $args['message'] ?? null, - 'website_id' => $websiteId, 'expires_at' => $args['expiresAt'] ?? null, ]); + $giftcard->setWebsiteIds($websiteIds); $giftcard->save(); $this->sendEmailToRecipient($giftcard); @@ -251,7 +258,7 @@ private function adjustBalance(array $context): GiftCard throw new NotFoundHttpException('Gift card not found'); } - $this->assertWebsiteAllowed($giftcard->getWebsiteId(), $this->requireUser(), 'gift card'); + $this->assertAllWebsitesAllowed($giftcard->getWebsiteIds(), $this->requireUser(), 'gift card'); $this->assertBalanceBounds($newBalance); $giftcard->adjustBalance($newBalance, $args['comment'] ?? null); diff --git a/app/code/core/Maho/Giftcard/Api/GiftCardProvider.php b/app/code/core/Maho/Giftcard/Api/GiftCardProvider.php index ea46876897..8ae6ceb383 100644 --- a/app/code/core/Maho/Giftcard/Api/GiftCardProvider.php +++ b/app/code/core/Maho/Giftcard/Api/GiftCardProvider.php @@ -36,10 +36,10 @@ protected function handleOperation(string $name, array $context, array $uriVaria } $giftcard = \Mage::getModel('giftcard/giftcard')->loadByCode(trim($code)); - // A card is only redeemable on its own website, so another - // website's store must not answer for it either. + // A card is only redeemable on the websites it is associated with, + // so any other website's store must not answer for it either. if (!$giftcard->getId() - || (int) $giftcard->getWebsiteId() !== (int) StoreContext::getStore()->getWebsiteId() + || !in_array((int) StoreContext::getStore()->getWebsiteId(), $giftcard->getWebsiteIds(), true) ) { throw new \RuntimeException('Gift card not found'); } @@ -51,28 +51,32 @@ protected function handleOperation(string $name, array $context, array $uriVaria } /** - * Admin/service item read, restricted tokens only see cards on their - * allowed websites. + * Admin/service item read, restricted tokens only see cards reaching at + * least one of their allowed websites. */ #[\Override] protected function provideItem(int|string $id): ?Resource { $dto = parent::provideItem($id); if ($dto instanceof GiftCard) { - $this->assertWebsiteAllowed($dto->websiteId, $this->requireUser(), 'gift card'); + $this->assertAnyWebsiteAllowed($dto->websiteIds ?? [], $this->requireUser(), 'gift card'); } return $dto; } /** * Admin/service list, restricted tokens only see cards on their allowed - * websites. + * websites. Membership lives in the giftcard_website junction, so this + * cannot use the shared main_table.website_id filter. */ #[\Override] protected function applyCollectionFilters(object $collection, array $filters): void { parent::applyCollectionFilters($collection, $filters); - $this->applyAllowedWebsiteFilter($collection, $this->requireUser()); + $allowedWebsiteIds = $this->allowedWebsiteIds($this->requireUser()); + if ($allowedWebsiteIds !== null) { + $collection->addWebsiteIdsFilter($allowedWebsiteIds); + } } /** diff --git a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Form.php b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Form.php index 1eae09492d..6caecf655e 100644 --- a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Form.php +++ b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Form.php @@ -8,266 +8,28 @@ declare(strict_types=1); +/** + * Empty `
` 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 = '[' . $defaultCurrency . ']'; - } 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 . '
' . 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 $websiteCurrencies - */ - protected function _getWebsiteCurrencyScript(array $websiteCurrencies): string - { - $currenciesJson = Mage::helper('core')->jsonEncode($websiteCurrencies); - - return << -(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(); - } -})(); - -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 = '
'; - - if ($helper->isQrCodeEnabled()) { - $qrUrl = $helper->getQrCodeDataUrl($model->getCode(), 200); - $html .= '
'; - $html .= '
QR Code (Scannable)
'; - $html .= 'QR Code'; - $html .= '
' . htmlspecialchars($model->getCode()) . '
'; - $html .= '
'; - } - - if ($helper->isBarcodeEnabled()) { - $barcodeUrl = $helper->getBarcodeDataUrl($model->getCode()); - $html .= '
'; - $html .= '
Barcode (Code128)
'; - $html .= 'Barcode'; - $html .= '
' . htmlspecialchars($model->getCode()) . '
'; - $html .= '
'; - } - - $html .= '
'; - - return $html; - } } diff --git a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/History.php b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/History.php new file mode 100644 index 0000000000..166b1cf0fc --- /dev/null +++ b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/History.php @@ -0,0 +1,102 @@ + + * SPDX-License-Identifier: OSL-3.0 + * @package Maho_Giftcard + */ + +declare(strict_types=1); + +/** + * Card-scoped transaction history shown as the second tab on the gift + * card edit page (registered via the adminhtml_giftcard_edit layout + * handle's ``). + * + * Subclasses the existing standalone history grid (which lists every + * card's history together for the Sales → Gift Card History page) and + * scopes its collection to the currently-loaded gift card so the audit + * trail lives next to the card it describes. + * + * Mass actions and row-click navigation are explicitly disabled — a row + * in this view is a read-only audit-trail entry, not a launchpad to + * another screen. + */ +class Maho_Giftcard_Block_Adminhtml_Giftcard_Edit_History extends Maho_Giftcard_Block_Adminhtml_Giftcard_History_Grid implements Mage_Adminhtml_Block_Widget_Tab_Interface +{ + #[\Override] + public function getTabLabel(): string + { + return Mage::helper('giftcard')->__('Transaction History'); + } + + #[\Override] + public function getTabTitle(): string + { + return Mage::helper('giftcard')->__('Transaction History'); + } + + #[\Override] + public function canShowTab(): bool + { + $model = Mage::registry('current_giftcard'); + return $model !== null && $model->getId() !== null; + } + + #[\Override] + public function isHidden(): bool + { + return false; + } + + public function __construct() + { + parent::__construct(); + // Distinct id from the standalone history grid so saved-session + // filters / sort state don't bleed across the two views. + $this->setId('giftcard_edit_history'); + $this->setUseAjax(true); + } + + /** + * Scope to the current gift card. Without a registered card we set a + * zero-id filter so the table is empty rather than leaking every + * history row. + */ + #[\Override] + protected function _prepareCollection() + { + $model = Mage::registry('current_giftcard'); + $cardId = $model && $model->getId() ? (int) $model->getId() : 0; + + $collection = Mage::getModel('giftcard/history')->getCollection() + ->addFieldToFilter('giftcard_id', $cardId); + + $this->setCollection($collection); + return Mage_Adminhtml_Block_Widget_Grid::_prepareCollection(); + } + + /** + * Suppress the standalone grid's mass actions — this view is read-only. + */ + #[\Override] + protected function _prepareMassaction() + { + return $this; + } + + #[\Override] + public function getRowUrl($row) + { + return ''; + } + + #[\Override] + public function getGridUrl() + { + $model = Mage::registry('current_giftcard'); + return $this->getUrl('*/*/historyGrid', [ + 'id' => $model ? $model->getId() : 0, + ]); + } +} diff --git a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Tab/Form.php b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Tab/Form.php new file mode 100644 index 0000000000..b0d520c4a2 --- /dev/null +++ b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Tab/Form.php @@ -0,0 +1,309 @@ + + * SPDX-License-Identifier: OSL-3.0 + * @package Maho_Giftcard + */ + +declare(strict_types=1); + +/** + * General-tab content for the gift card edit page: all of the editable + * fields (code, status, websites multiselect, balance, expiry, recipient, + * sender, message, admin comment, plus QR/barcode display for existing + * cards). Renders no `` wrapper — the parent Edit/Form.php carries + * `` and the tabs JS injects this fieldset into it. + */ +class Maho_Giftcard_Block_Adminhtml_Giftcard_Edit_Tab_Form extends Mage_Adminhtml_Block_Widget_Form implements Mage_Adminhtml_Block_Widget_Tab_Interface +{ + #[\Override] + public function getTabLabel(): string + { + return Mage::helper('giftcard')->__('General'); + } + + #[\Override] + public function getTabTitle(): string + { + return Mage::helper('giftcard')->__('General'); + } + + #[\Override] + public function canShowTab(): bool + { + return true; + } + + #[\Override] + public function isHidden(): bool + { + return false; + } + + #[\Override] + protected function _prepareForm() + { + $model = Mage::registry('current_giftcard'); + + $form = new Maho\Data\Form(); + + $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. Multiselect so a card can + // be valid on more than one website; on save the controller persists + // these via setWebsiteIds() into the giftcard_website junction. + $websites = Mage::app()->getWebsites(); + $websiteCurrencies = []; + $websiteValues = []; + foreach ($websites as $website) { + $websiteValues[$website->getId()] = $website->getName(); + $websiteCurrencies[$website->getId()] = $website->getBaseCurrencyCode(); + } + + if (!$model->getId()) { + $defaultWebsiteId = (int) array_key_first($websiteCurrencies); + $defaultSelection = [$defaultWebsiteId]; + $defaultCurrency = $websiteCurrencies[$defaultWebsiteId] ?? ''; + $currencyNote = '[' . $defaultCurrency . ']'; + } else { + // Existing card — pre-select the websites already associated via + // the junction. Editable so an admin can re-scope after the fact. + $defaultSelection = $model->getWebsiteIds(); + $currencyNote = '[' . $model->getCurrencyCode() . ']'; + } + + $fieldset->addField('website_ids', 'multiselect', [ + 'name' => 'website_ids[]', + 'label' => Mage::helper('giftcard')->__('Websites'), + 'title' => Mage::helper('giftcard')->__('Websites'), + 'required' => true, + 'values' => array_map( + static fn($id, $name) => ['value' => (int) $id, 'label' => $name], + array_keys($websiteValues), + $websiteValues, + ), + 'value' => $defaultSelection, + 'note' => Mage::helper('giftcard')->__('Hold Ctrl/Cmd to select multiple. All selected websites must share the same base currency; the card balance is denominated in it.'), + 'after_element_html' => $this->_getWebsiteCurrencyScript($websiteCurrencies), + ]); + + 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 = $model->getWebsite(); + $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 . '
' . 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), + ]); + } + + // DB stores balance/initial_balance as decimal(12,4) for arithmetic + // precision (post-tax, currency conversion, partial redemption math). + // The admin form shouldn't expose the trailing zeros — admins enter + // and review prices to 2dp. + $data = $model->getData(); + foreach (['balance', 'initial_balance'] as $field) { + if (isset($data[$field]) && $data[$field] !== '') { + $data[$field] = number_format((float) $data[$field], 2, '.', ''); + } + } + $form->setValues($data); + $this->setForm($form); + + return parent::_prepareForm(); + } + + /** + * Get JavaScript for updating currency display when website changes + * + * @param array $websiteCurrencies + */ + protected function _getWebsiteCurrencyScript(array $websiteCurrencies): string + { + $currenciesJson = Mage::helper('core')->jsonEncode($websiteCurrencies); + + return << +(function() { + const websiteCurrencies = {$currenciesJson}; + + function updateCurrencyDisplay() { + const websiteSelect = document.getElementById('website_ids'); + if (!websiteSelect) return; + + const websiteId = websiteSelect.selectedOptions[0] ? websiteSelect.selectedOptions[0].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_ids'); + if (websiteSelect) { + websiteSelect.addEventListener('change', updateCurrencyDisplay); + } + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } +})(); + +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 = '
'; + + if ($helper->isQrCodeEnabled()) { + $qrUrl = $helper->getQrCodeDataUrl($model->getCode(), 200); + $html .= '
'; + $html .= '
QR Code (Scannable)
'; + $html .= 'QR Code'; + $html .= '
' . htmlspecialchars($model->getCode()) . '
'; + $html .= '
'; + } + + if ($helper->isBarcodeEnabled()) { + $barcodeUrl = $helper->getBarcodeDataUrl($model->getCode()); + $html .= '
'; + $html .= '
Barcode (Code128)
'; + $html .= 'Barcode'; + $html .= '
' . htmlspecialchars($model->getCode()) . '
'; + $html .= '
'; + } + + $html .= '
'; + + return $html; + } +} diff --git a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Tabs.php b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Tabs.php new file mode 100644 index 0000000000..633e2d3e6f --- /dev/null +++ b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Edit/Tabs.php @@ -0,0 +1,31 @@ + + * SPDX-License-Identifier: OSL-3.0 + * @package Maho_Giftcard + */ + +declare(strict_types=1); + +/** + * Tabs container for the gift card edit page. Wired into the page via the + * adminhtml_giftcard_edit layout handle, which adds it as a child of the + * `left` reference and registers each tab via ``. + * + * Mirrors the canonical CMS Page Edit pattern — the Form_Container in the + * `content` reference renders the edit chrome (header + save buttons) and + * the empty `` wrapper; this Tabs block then injects + * each tab's content (form fieldsets / history grid) into that form via + * the `destElementId` hook. + */ +class Maho_Giftcard_Block_Adminhtml_Giftcard_Edit_Tabs extends Mage_Adminhtml_Block_Widget_Tabs +{ + public function __construct() + { + parent::__construct(); + $this->setId('giftcard_edit_tabs'); + $this->setDestElementId('edit_form'); + $this->setTitle(Mage::helper('giftcard')->__('Gift Card Information')); + } +} diff --git a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Grid.php b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Grid.php index 6077bb9ead..5265f5bec0 100644 --- a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Grid.php +++ b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Grid.php @@ -23,8 +23,21 @@ public function __construct() protected function _prepareCollection() { $collection = Mage::getModel('giftcard/giftcard')->getCollection(); - $this->setCollection($collection); + // Aggregate the website associations from the junction so the grid + // shows / sorts / filters on a single GROUP_CONCAT column. LEFT JOIN + // so cards that haven't been backfilled yet (or were stripped of all + // associations by a botched edit) still appear in the listing — they + // render as "—" and the operator can re-scope them from the edit page. + $select = $collection->getSelect(); + $junction = $collection->getTable('giftcard/website'); + $select->joinLeft( + ['gw' => $junction], + 'gw.giftcard_id = main_table.giftcard_id', + ['website_ids' => new Maho\Db\Expr('GROUP_CONCAT(DISTINCT gw.website_id ORDER BY gw.website_id ASC)')], + )->group('main_table.giftcard_id'); + + $this->setCollection($collection); return parent::_prepareCollection(); } @@ -59,13 +72,21 @@ protected function _prepareColumns() ]); if (!Mage::app()->isSingleStoreMode()) { - $this->addColumn('website_id', [ - 'header' => Mage::helper('giftcard')->__('Website'), - 'align' => 'left', - 'width' => '100px', - 'index' => 'website_id', - 'type' => 'options', - 'options' => Mage::getSingleton('adminhtml/system_store')->getWebsiteOptionHash(), + // Multi-website column backed by the giftcard_website junction + // (see _prepareCollection). The GROUP_CONCAT'd value renders as a + // comma-separated list of website names; the filter is FIND_IN_SET + // against the same expression so the operator can scope the grid + // to "cards valid on website N". + $this->addColumn('website_ids', [ + 'header' => Mage::helper('giftcard')->__('Websites'), + 'align' => 'left', + 'width' => '160px', + 'index' => 'website_ids', + 'type' => 'options', + 'options' => Mage::getSingleton('adminhtml/system_store')->getWebsiteOptionHash(), + 'sortable' => false, + 'renderer' => Maho_Giftcard_Block_Adminhtml_Giftcard_Renderer_Websites::class, + 'filter_condition_callback' => $this->_filterWebsiteCondition(...), ]); } @@ -186,4 +207,25 @@ public function getRowUrl($row) { return $this->getUrl('*/*/edit', ['id' => $row->getId()]); } + + /** + * Filter the grid by membership in the giftcard_website junction. + * + * The column dropdown sends a single website_id; we translate to a + * FIND_IN_SET against the GROUP_CONCAT'd alias built in _prepareCollection + * (HAVING because the alias is computed, not a raw column reference). + * + * @param Maho_Giftcard_Model_Resource_Giftcard_Collection $collection + * @param Mage_Adminhtml_Block_Widget_Grid_Column $column + */ + protected function _filterWebsiteCondition($collection, $column): void + { + $value = $column->getFilter()->getValue(); + if ($value === null || $value === '') { + return; + } + $collection->getSelect()->having( + sprintf('FIND_IN_SET(%d, website_ids) > 0', (int) $value), + ); + } } diff --git a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/History/Grid.php b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/History/Grid.php index 1c16c1c47c..b374bb0c2a 100644 --- a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/History/Grid.php +++ b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/History/Grid.php @@ -24,11 +24,22 @@ protected function _prepareCollection() { $collection = Mage::getModel('giftcard/history')->getCollection(); - // Join gift card table to get code and website_id (for currency lookup) + // Join gift card table to get code, plus a representative website id + // for the currency lookup: the card's associated websites all share + // one base currency (enforced on save), so MIN() over the junction is + // a valid currency source. A scalar subquery instead of a join keeps + // history rows from being duplicated per associated website. $collection->getSelect()->join( ['gc' => $collection->getTable('giftcard/giftcard')], 'main_table.giftcard_id = gc.giftcard_id', - ['code', 'recipient_email', 'website_id'], + [ + 'code', + 'recipient_email', + 'website_id' => new Maho\Db\Expr(sprintf( + '(SELECT MIN(gw.website_id) FROM %s gw WHERE gw.giftcard_id = gc.giftcard_id)', + $collection->getTable('giftcard/website'), + )), + ], ); // Join order table to get increment_id diff --git a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Renderer/Currency.php b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Renderer/Currency.php index f8b7485054..31ea18dc82 100644 --- a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Renderer/Currency.php +++ b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Renderer/Currency.php @@ -33,11 +33,20 @@ public function render(Maho\DataObject $row) } /** - * Get currency code for the gift card row + * Get currency code for the gift card row. + * + * Grid collections carry the card's website associations either as the + * aggregated `website_ids` CSV (main grid GROUP_CONCAT) or as a single + * representative `website_id` (history grid subquery). All associated + * websites share one base currency (enforced on save), so the first id + * of either form is a valid currency source. */ protected function _getCurrencyCode(Maho\DataObject $row): string { $websiteId = $row->getData('website_id'); + if (!$websiteId && ($csv = (string) $row->getData('website_ids')) !== '') { + $websiteId = (int) explode(',', $csv)[0]; + } if ($websiteId) { try { return Mage::app()->getWebsite($websiteId)->getBaseCurrencyCode(); diff --git a/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Renderer/Websites.php b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Renderer/Websites.php new file mode 100644 index 0000000000..f007208217 --- /dev/null +++ b/app/code/core/Maho/Giftcard/Block/Adminhtml/Giftcard/Renderer/Websites.php @@ -0,0 +1,42 @@ + + * SPDX-License-Identifier: OSL-3.0 + * @package Maho_Giftcard + */ + +declare(strict_types=1); + +class Maho_Giftcard_Block_Adminhtml_Giftcard_Renderer_Websites extends Mage_Adminhtml_Block_Widget_Grid_Column_Renderer_Abstract +{ + /** + * Render the grid's "Websites" cell from the GROUP_CONCAT'd website_ids + * alias built in Grid::_prepareCollection(). + * + * Looks up display names from the admin system-store helper once per + * render call and maps each comma-separated id; a row with no junction + * entries (NULL alias) renders as an em dash so the listing still + * surfaces orphaned cards rather than hiding them. + */ + #[\Override] + public function render(Maho\DataObject $row) + { + $raw = (string) $row->getData('website_ids'); + if ($raw === '') { + return ''; + } + + $hash = Mage::getSingleton('adminhtml/system_store')->getWebsiteOptionHash(); + $names = []; + foreach (explode(',', $raw) as $id) { + $id = trim($id); + if ($id === '') { + continue; + } + $names[] = $this->escapeHtml($hash[(int) $id] ?? '[id ' . $id . ']'); + } + + return implode(', ', $names); + } +} diff --git a/app/code/core/Maho/Giftcard/Block/Customer/Balance.php b/app/code/core/Maho/Giftcard/Block/Customer/Balance.php new file mode 100644 index 0000000000..6f0ffb198c --- /dev/null +++ b/app/code/core/Maho/Giftcard/Block/Customer/Balance.php @@ -0,0 +1,55 @@ + + * SPDX-License-Identifier: OSL-3.0 + * @package Maho_Giftcard + */ + +declare(strict_types=1); + +/** + * Block for the customer-facing gift card balance lookup page. + * + * Pulls the most recent lookup result out of giftcard/session (set by + * BalanceController::checkAction) so the template can render the balance + * panel below the form without the result surviving across navigations or + * leaking through browser history. + */ +class Maho_Giftcard_Block_Customer_Balance extends Mage_Core_Block_Template +{ + /** + * @return array{code: string, balance: float, currency_code: string, expires_at: ?string}|null + */ + public function getLastLookup(): ?array + { + $session = Mage::getSingleton('giftcard/session'); + $data = $session->getLastGiftcardLookup(); + if (!is_array($data) || !isset($data['code'], $data['balance'], $data['currency_code'])) { + return null; + } + // One-shot: consume on render so a back/forward navigation doesn't + // re-display the previous customer's check result if the customer + // shares a device. + $session->setLastGiftcardLookup(null); + return [ + 'code' => (string) $data['code'], + 'balance' => (float) $data['balance'], + 'currency_code' => (string) $data['currency_code'], + 'expires_at' => isset($data['expires_at']) ? (string) $data['expires_at'] : null, + ]; + } + + public function getCheckUrl(): string + { + return $this->getUrl('giftcard/balance/check'); + } + + public function formatBalance(float $amount, string $currency): string + { + // PHP NumberFormatter (Maho's replacement for the removed Zend_Currency) + // takes (amount, currency_code) on formatCurrency rather than the old + // Zend_Currency->toCurrency($amount) call. + return Mage::app()->getLocale()->currency($currency)->formatCurrency($amount, $currency); + } +} diff --git a/app/code/core/Maho/Giftcard/Model/Giftcard.php b/app/code/core/Maho/Giftcard/Model/Giftcard.php index dbb079ad55..a4ce6c7764 100644 --- a/app/code/core/Maho/Giftcard/Model/Giftcard.php +++ b/app/code/core/Maho/Giftcard/Model/Giftcard.php @@ -15,8 +15,6 @@ * @method $this setCode(string $value) * @method string getStatus() * @method $this setStatus(string $value) - * @method int getWebsiteId() - * @method $this setWebsiteId(int $value) * @method $this setBalance(float $value) * @method float getInitialBalance() * @method $this setInitialBalance(float $value) @@ -71,8 +69,11 @@ protected function _beforeSave() $this->setCode($helper->generateCode()); } - if (!$this->getWebsiteId()) { - $this->setWebsiteId((int) Mage::app()->getStore()->getWebsiteId()); + // Default to the current website when no associations were set, + // so programmatic creations and imports never produce an orphaned + // card that fails closed on every website. + if ($this->getData('website_ids') === null) { + $this->setWebsiteIds([(int) Mage::app()->getStore()->getWebsiteId()]); } // Only fill a default when the field wasn't provided. Explicit null @@ -121,11 +122,16 @@ public function loadByCode(string $code): self } /** - * Get website + * Get the card's website: the first (lowest-id) website it is associated + * with. All associated websites are required to share one base currency + * (enforced on save), so any of them is a valid currency source; the + * lowest id keeps the choice deterministic. Falls back to the default + * website for an orphaned card with no associations. */ public function getWebsite(): Mage_Core_Model_Website { - return Mage::app()->getWebsite($this->getWebsiteId()); + $websiteIds = $this->getWebsiteIds(); + return Mage::app()->getWebsite($websiteIds[0] ?? null); } /** @@ -189,6 +195,11 @@ public function isValid(): bool /** * Check if gift card is valid for use on a specific website + * + * Membership lookup against the giftcard_website junction. A card with no + * websites assigned (orphaned from a botched import or hand-crafted row) + * is treated as not valid anywhere — fail closed rather than opening the + * card up to every website silently. */ public function isValidForWebsite(int $websiteId): bool { @@ -196,7 +207,62 @@ public function isValidForWebsite(int $websiteId): bool return false; } - return (int) $this->getWebsiteId() === $websiteId; + return in_array($websiteId, $this->getWebsiteIds(), true); + } + + /** + * Lazily-loaded snapshot of the card's junction rows. Deliberately kept + * out of the `website_ids` data key: that key means "pending change to + * persist" to the resource's _afterSave sync, so caching a mere read + * there would re-sync the junction on every subsequent save (checkout + * and refund call getWebsiteIds() before saving balance changes). + * + * @var int[]|null + */ + private ?array $loadedWebsiteIds = null; + + /** + * Get the list of website IDs this card is valid on. + * + * Returns the pending set from setWebsiteIds() when one exists, otherwise + * lazy-loads the current associations from the giftcard_website junction. + * + * @return int[] + */ + public function getWebsiteIds(): array + { + $ids = $this->getData('website_ids'); + if ($ids !== null) { + return array_map(intval(...), (array) $ids); + } + if (!$this->getId()) { + return []; + } + if ($this->loadedWebsiteIds === null) { + /** @var Maho_Giftcard_Model_Resource_Giftcard $resource */ + $resource = $this->getResource(); + $this->loadedWebsiteIds = $resource->getWebsiteIds((int) $this->getId()); + } + return $this->loadedWebsiteIds; + } + + /** + * Set the list of website IDs this card is valid on. The resource + * model's _afterSave hook persists the change to the junction. + * + * @param int[] $websiteIds + */ + public function setWebsiteIds(array $websiteIds): self + { + $clean = []; + foreach ($websiteIds as $id) { + $id = (int) $id; + if ($id > 0) { + $clean[$id] = true; + } + } + $this->setData('website_ids', array_keys($clean)); + return $this; } /** diff --git a/app/code/core/Maho/Giftcard/Model/Observer.php b/app/code/core/Maho/Giftcard/Model/Observer.php index 98c6a1c05a..7f4d6a8b6e 100644 --- a/app/code/core/Maho/Giftcard/Model/Observer.php +++ b/app/code/core/Maho/Giftcard/Model/Observer.php @@ -104,7 +104,6 @@ protected function _createGiftcard( $giftcard->setData([ 'code' => $helper->generateCode(), 'status' => Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE, - 'website_id' => $website->getId(), 'balance' => $baseAmount, 'initial_balance' => $baseAmount, 'recipient_name' => $recipientName, @@ -118,6 +117,9 @@ protected function _createGiftcard( 'created_at' => Mage::app()->getLocale()->formatDateForDb('now'), 'updated_at' => Mage::app()->getLocale()->formatDateForDb('now'), ]); + // Purchased cards start valid on the website they were bought on; + // an admin can broaden the association from the card's edit page. + $giftcard->setWebsiteIds([(int) $website->getId()]); $giftcard->save(); @@ -833,7 +835,7 @@ public function processAdminOrderGiftcard(Maho\Event\Observer $observer): void // Check website validity $websiteId = (int) $quote->getStore()->getWebsiteId(); - if ((int) $giftcard->getWebsiteId() !== $websiteId) { + if (!in_array($websiteId, $giftcard->getWebsiteIds(), true)) { throw new Mage_Core_Exception( Mage::helper('giftcard')->__('Gift card "%s" is not valid for this website.', $code), ); diff --git a/app/code/core/Maho/Giftcard/Model/Resource/Giftcard.php b/app/code/core/Maho/Giftcard/Model/Resource/Giftcard.php index 7243b30c08..20ce2fbc34 100644 --- a/app/code/core/Maho/Giftcard/Model/Resource/Giftcard.php +++ b/app/code/core/Maho/Giftcard/Model/Resource/Giftcard.php @@ -51,4 +51,87 @@ protected function _beforeSave(Mage_Core_Model_Abstract $object) return parent::_beforeSave($object); } + + /** + * Sync the giftcard_website junction whenever the model is saved. + * + * The admin form (and any other caller using setWebsiteIds) sets a + * `website_ids` data key on the model. We delete the previous rows for + * this card and insert the new set in one transaction, so a save either + * lands cleanly or leaves the junction in its previous state if the + * INSERT fails partway. Skipped when no `website_ids` was set, so a + * background save that only touched, say, the balance doesn't trample + * the website associations. + */ + #[\Override] + protected function _afterSave(Mage_Core_Model_Abstract $object) + { + $ids = $object->getData('website_ids'); + if (is_array($ids)) { + // Fail closed on an explicit empty set. isValidForWebsite() rejects + // a card with no associations on every website, so silently + // accepting an empty set here would orphan the card with no admin + // surfacing of the change. Callers that genuinely want to + // de-associate everything should delete the card instead. + if (empty($ids)) { + throw new Mage_Core_Exception( + Mage::helper('giftcard')->__('A gift card must be associated with at least one website.'), + ); + } + + // A card's balance is denominated in one currency, so every + // associated website must share the same base currency — otherwise + // the balance would silently mean different amounts per website. + $currencies = []; + foreach ($ids as $websiteId) { + $currencies[Mage::app()->getWebsite((int) $websiteId)->getBaseCurrencyCode()] = true; + } + if (count($currencies) > 1) { + throw new Mage_Core_Exception( + Mage::helper('giftcard')->__('A gift card can only be assigned to websites that share the same base currency.'), + ); + } + + $adapter = $this->_getWriteAdapter(); + $table = $this->getTable('giftcard/website'); + $giftcardId = (int) $object->getId(); + + $adapter->beginTransaction(); + try { + $adapter->delete($table, ['giftcard_id = ?' => $giftcardId]); + $rows = []; + foreach ($ids as $websiteId) { + $rows[] = [ + 'giftcard_id' => $giftcardId, + 'website_id' => (int) $websiteId, + ]; + } + $adapter->insertMultiple($table, $rows); + $adapter->commit(); + } catch (\Throwable $e) { + $adapter->rollBack(); + throw $e; + } + } + + return parent::_afterSave($object); + } + + /** + * Fetch the website IDs this card is valid on, ordered. + * + * @return int[] + */ + public function getWebsiteIds(int $giftcardId): array + { + if ($giftcardId <= 0) { + return []; + } + $adapter = $this->_getReadAdapter(); + $select = $adapter->select() + ->from($this->getTable('giftcard/website'), ['website_id']) + ->where('giftcard_id = ?', $giftcardId) + ->order('website_id ASC'); + return array_map(intval(...), $adapter->fetchCol($select)); + } } diff --git a/app/code/core/Maho/Giftcard/Model/Resource/Giftcard/Collection.php b/app/code/core/Maho/Giftcard/Model/Resource/Giftcard/Collection.php index 3d4063c2f1..3724cd0912 100644 --- a/app/code/core/Maho/Giftcard/Model/Resource/Giftcard/Collection.php +++ b/app/code/core/Maho/Giftcard/Model/Resource/Giftcard/Collection.php @@ -37,4 +37,39 @@ public function addOrderFilter(int $orderId): self $this->addFieldToFilter('purchase_order_id', $orderId); return $this; } + + /** + * Filter to cards valid on the given website, by membership in the + * giftcard_website junction. A correlated subquery instead of a join, so + * the filter composes with unqualified addFieldToFilter() column names + * (a join would make shared columns like giftcard_id ambiguous). + * + * @return $this + */ + public function addWebsiteFilter(int $websiteId): self + { + $this->getSelect()->where( + 'main_table.giftcard_id IN (SELECT giftcard_id FROM ' + . $this->getTable('giftcard/website') . ' WHERE website_id = ?)', + $websiteId, + ); + return $this; + } + + /** + * Filter to cards valid on any of the given websites. An empty list matches + * nothing, so a token scoped to no website sees no cards. + * + * @param int[] $websiteIds + * @return $this + */ + public function addWebsiteIdsFilter(array $websiteIds): self + { + $this->getSelect()->where( + 'main_table.giftcard_id IN (SELECT giftcard_id FROM ' + . $this->getTable('giftcard/website') . ' WHERE website_id IN (?))', + $websiteIds === [] ? [-1] : array_map(intval(...), $websiteIds), + ); + return $this; + } } diff --git a/app/code/core/Maho/Giftcard/Model/Session.php b/app/code/core/Maho/Giftcard/Model/Session.php new file mode 100644 index 0000000000..c03ca566f7 --- /dev/null +++ b/app/code/core/Maho/Giftcard/Model/Session.php @@ -0,0 +1,22 @@ + + * SPDX-License-Identifier: OSL-3.0 + * @package Maho_Giftcard + */ + +declare(strict_types=1); + +/** + * Per-customer session namespace used by the My Account balance lookup + * page to hand the most recent check result from the POST handler to the + * GET renderer, and to surface user-facing error messages. + */ +class Maho_Giftcard_Model_Session extends Mage_Core_Model_Session_Abstract +{ + public function __construct() + { + $this->init('giftcard'); + } +} diff --git a/app/code/core/Maho/Giftcard/controllers/Adminhtml/GiftcardController.php b/app/code/core/Maho/Giftcard/controllers/Adminhtml/GiftcardController.php index 5963d46d7b..85c89b1713 100644 --- a/app/code/core/Maho/Giftcard/controllers/Adminhtml/GiftcardController.php +++ b/app/code/core/Maho/Giftcard/controllers/Adminhtml/GiftcardController.php @@ -54,6 +54,31 @@ public function gridAction(): void $this->renderLayout(); } + /** + * AJAX grid action for the Transaction History tab on the edit page. + * + * Registers the current gift card so the tab block scopes its + * collection correctly, then renders the grid in isolation (Mage's + * Tabs widget reloads the inner grid via this URL on page filter/sort). + */ + #[Maho\Config\Route('/admin/giftcard/historyGrid')] + public function historyGridAction(): void + { + $model = Mage::getModel('giftcard/giftcard'); + $id = (int) $this->getRequest()->getParam('id'); + if ($id > 0) { + $model->load($id); + } + Mage::register('current_giftcard', $model); + + $this->loadLayout(); + $this->getResponse()->setBody( + $this->getLayout() + ->createBlock('giftcard/adminhtml_giftcard_edit_history') + ->toHtml(), + ); + } + /** * New gift card */ @@ -93,8 +118,11 @@ public function editAction(): void Mage::register('current_giftcard', $model); + // Layout XML handle adminhtml_giftcard_edit registers the Form_Container + // in `content` and the Tabs block in `left`; loadLayout() picks both + // up automatically via the handle. The form and the transaction-history + // grid render as separate tabs of the same edit form. $this->_initAction(); - $this->_addContent($this->getLayout()->createBlock('giftcard/adminhtml_giftcard_edit')); $this->renderLayout(); } @@ -113,15 +141,39 @@ public function saveAction(): void } try { + // Normalise the multiselect post: the form sends website_ids[] + // (or, if the JS is bypassed, sometimes a single value). + $websiteIds = $data['website_ids'] ?? []; + if (!is_array($websiteIds)) { + $websiteIds = $websiteIds === '' ? [] : [$websiteIds]; + } + $websiteIds = array_values(array_unique(array_filter(array_map(intval(...), $websiteIds)))); + if (empty($websiteIds)) { + // Nothing posted (e.g. an old form, an API caller) — fall + // back to the current admin website so saves never land + // with an empty association set that would orphan the card. + $websiteIds = [(int) Mage::app()->getWebsite()->getId()]; + } + $data['website_ids'] = $websiteIds; + // If balance changed on existing card, record as adjustment $oldBalance = (float) $model->getBalance(); $newBalance = isset($data['balance']) ? (float) $data['balance'] : $oldBalance; + $isBalanceAdjustment = $model->getId() && $oldBalance != $newBalance; + + if ($isBalanceAdjustment) { + // Keep the model on the old balance through this first save so + // adjustBalance() (below) computes the correct delta and writes + // the new balance plus an accurate history entry. Setting the + // new balance here would make adjustBalance() see a zero change. + $data['balance'] = $oldBalance; + } $model->setData($data); $model->save(); // Record balance adjustment if changed - if ($model->getId() && $oldBalance != $newBalance) { + if ($isBalanceAdjustment) { $model->adjustBalance($newBalance, $data['comment'] ?? 'Admin adjustment'); } @@ -284,7 +336,7 @@ public function checkBalanceAction(): void 'balance' => $giftcard->getBalance(), 'initial_balance' => $giftcard->getInitialBalance(), 'currency_code' => $giftcard->getCurrencyCode(), - 'website_id' => $giftcard->getWebsiteId(), + 'website_ids' => $giftcard->getWebsiteIds(), 'status' => $giftcard->getStatus(), 'is_valid' => $giftcard->isValid(), 'expires_at' => $giftcard->getExpiresAt(), diff --git a/app/code/core/Maho/Giftcard/controllers/BalanceController.php b/app/code/core/Maho/Giftcard/controllers/BalanceController.php new file mode 100644 index 0000000000..be3bda756a --- /dev/null +++ b/app/code/core/Maho/Giftcard/controllers/BalanceController.php @@ -0,0 +1,130 @@ + + * SPDX-License-Identifier: OSL-3.0 + * @package Maho_Giftcard + */ + +declare(strict_types=1); + +/** + * Customer-facing "Check Gift Card Balance" page. + * + * Lives under My Account (authentication enforced in preDispatch); the + * customer enters a gift card code and sees the remaining balance + expiry, + * scoped to the websites this card is valid on. No transaction-history view + * — that's intentionally admin-only for traceability without exposing + * internal balance-adjustment comments to the customer. + */ +class Maho_Giftcard_BalanceController extends Mage_Core_Controller_Front_Action +{ + /** + * Force-login the customer; matches the behaviour of every other + * customer/account/* action so the page slots into the My Account menu + * without surprising visitors who follow a deep link while signed out. + */ + #[\Override] + public function preDispatch() + { + parent::preDispatch(); + + if (!$this->getRequest()->isDispatched()) { + return $this; + } + + $action = strtolower((string) $this->getRequest()->getActionName()); + if (in_array($action, ['index', 'check'], true) + && !Mage::getSingleton('customer/session')->authenticate($this) + ) { + $this->setFlag('', self::FLAG_NO_DISPATCH, true); + } + + return $this; + } + + /** + * Render the lookup form. The result of the most recent POST (if any) + * is read out of customer/session by the block — using session storage + * rather than passing through query params keeps a previously-checked + * code from leaking into browser history / referer logs. + */ + #[Maho\Config\Route('/giftcard/balance', methods: ['GET'])] + public function indexAction(): void + { + $this->loadLayout(); + // Flash errors use customer/session (consumed by the standard My + // Account messages block on render); giftcard/session carries only + // the lookup result payload, not user-facing messages, so it doesn't + // need wiring into the messages block. + $this->_initLayoutMessages('customer/session'); + $this->renderLayout(); + } + + /** + * Look up a posted code. Treats "not found", "expired", "disabled" and + * "no website membership" as the same opaque "could not be found" + * outcome so this endpoint can't be used to enumerate which codes are + * live — a customer who genuinely owns a code will see it; an attacker + * walking codes can't distinguish "expired" from "doesn't exist". + * + * Rate-limited to 10 failed attempts per hour per customer via the + * shared Maho\Security\RateLimiter (`Mage_Core_Helper_Data::rateLimiterBy`). + * "Check upfront, hit only on failure" pattern so a customer with + * several legitimate cards isn't penalised for genuine lookups. + */ + #[Maho\Config\Route('/giftcard/balance/check', methods: ['POST'])] + public function checkAction(): void + { + // Frontend POSTs are not form-key-validated automatically; do it + // explicitly (as CartController does) so a cross-site POST can't burn + // the victim's rate-limit slots and lock them out of the feature. + if (!$this->_validateFormKey()) { + $this->_redirect('*/*/'); + return; + } + + $session = Mage::getSingleton('giftcard/session'); + $session->setLastGiftcardLookup(null); + $flash = Mage::getSingleton('customer/session'); + + $customerId = (string) Mage::getSingleton('customer/session')->getCustomerId(); + $limiter = Mage::helper('core')->rateLimiterBy('giftcard_balance_check', $customerId, 10, 3600); + if ($limiter->tooManyAttempts()) { + $flash->addError(Mage::helper('giftcard')->__('Too many recent lookup attempts. Please wait a while before trying again.')); + $this->_redirect('*/*/'); + return; + } + + $code = trim((string) $this->getRequest()->getPost('giftcard_code', '')); + if ($code === '') { + $flash->addError(Mage::helper('giftcard')->__('Please enter a gift card code.')); + $this->_redirect('*/*/'); + return; + } + + /** @var Maho_Giftcard_Model_Giftcard $card */ + $card = Mage::getModel('giftcard/giftcard'); + /** @var Maho_Giftcard_Model_Resource_Giftcard $resource */ + $resource = $card->getResource(); + $resource->loadByCode($card, $code); + + $websiteId = (int) Mage::app()->getStore()->getWebsiteId(); + + if (!$card->getId() || !$card->isValidForWebsite($websiteId)) { + $limiter->hit(); + $flash->addError(Mage::helper('giftcard')->__('We could not find an active gift card for that code on this store.')); + $this->_redirect('*/*/'); + return; + } + + $session->setLastGiftcardLookup([ + 'code' => $card->getCode(), + 'balance' => (float) $card->getBalance(), + 'currency_code' => (string) $card->getCurrencyCode(), + 'expires_at' => $card->getExpiresAt(), + ]); + + $this->_redirect('*/*/'); + } +} diff --git a/app/code/core/Maho/Giftcard/controllers/CartController.php b/app/code/core/Maho/Giftcard/controllers/CartController.php index c570054410..ce02004962 100644 --- a/app/code/core/Maho/Giftcard/controllers/CartController.php +++ b/app/code/core/Maho/Giftcard/controllers/CartController.php @@ -101,7 +101,7 @@ public function checkBalanceAction(): void } $websiteId = (int) Mage::app()->getStore()->getWebsiteId(); - if ((int) $giftcard->getWebsiteId() !== $websiteId) { + if (!in_array($websiteId, $giftcard->getWebsiteIds(), true)) { $result['message'] = $this->__('Gift card not found.'); $this->_sendJsonResponse($result); return; @@ -173,7 +173,7 @@ public function applyAction(): void // Check website validity $websiteId = (int) $quote->getStore()->getWebsiteId(); - if ((int) $giftcard->getWebsiteId() !== $websiteId) { + if (!in_array($websiteId, $giftcard->getWebsiteIds(), true)) { Mage::throwException($this->__('Gift card "%s" is not valid.', $code)); } @@ -317,7 +317,7 @@ public function ajaxApplyAction(): void // Check website validity $websiteId = (int) $quote->getStore()->getWebsiteId(); - if ((int) $giftcard->getWebsiteId() !== $websiteId) { + if (!in_array($websiteId, $giftcard->getWebsiteIds(), true)) { $result['message'] = $this->__('Gift card "%s" is not valid.', $code); $this->_sendJsonResponse($result); return; diff --git a/app/code/core/Maho/Giftcard/etc/config.xml b/app/code/core/Maho/Giftcard/etc/config.xml index 7a822e0738..590b276e9c 100644 --- a/app/code/core/Maho/Giftcard/etc/config.xml +++ b/app/code/core/Maho/Giftcard/etc/config.xml @@ -2,7 +2,7 @@ - 1.0.0 + 1.1.0 @@ -31,6 +31,9 @@ giftcard_history
+ + giftcard_website
+
diff --git a/app/code/core/Maho/Giftcard/sql/giftcard_setup/upgrade-1.0.0-1.1.0.php b/app/code/core/Maho/Giftcard/sql/giftcard_setup/upgrade-1.0.0-1.1.0.php new file mode 100644 index 0000000000..ace1c089a1 --- /dev/null +++ b/app/code/core/Maho/Giftcard/sql/giftcard_setup/upgrade-1.0.0-1.1.0.php @@ -0,0 +1,47 @@ + + * SPDX-License-Identifier: OSL-3.0 + * @package Maho_Giftcard + */ + +declare(strict_types=1); + +/** @var Mage_Core_Model_Resource_Setup $this */ +$installer = $this; +$installer->startSetup(); + +$connection = $installer->getConnection(); +$giftcardTable = $installer->getTable('giftcard/giftcard'); +$junctionTable = $installer->getTable('giftcard/website'); + +// 1.0.0 → 1.1.0: move the single-website association into the giftcard_website +// junction, then retire the scalar. The declarative pass has already run at +// this point (it creates the junction and, because `website_id` is no longer +// declared, drops its index/FK while preserving the column and its data — +// additive merge), so this script owns the data move and the column drop. +// +// Conservative backfill — one row per card, its original website — preserves +// pre-1.1.0 behaviour exactly: validation was a strict website match, so a +// card stays spendable only where it was before. Operators can broaden a +// card's websites from its admin page after the upgrade. +// +// Guarded by column existence, so fresh installs (where the declarative +// schema never creates the column) skip the whole block, and a re-run after +// the drop is a no-op. The LEFT JOIN keeps the INSERT idempotent if the +// script is interrupted between the backfill and the drop. +if ($connection->tableColumnExists($giftcardTable, 'website_id')) { + $connection->query( + "INSERT INTO {$junctionTable} (giftcard_id, website_id) + SELECT g.giftcard_id, g.website_id + FROM {$giftcardTable} g + LEFT JOIN {$junctionTable} gw + ON gw.giftcard_id = g.giftcard_id AND gw.website_id = g.website_id + WHERE g.website_id > 0 AND gw.giftcard_id IS NULL", + ); + + $connection->dropColumn($giftcardTable, 'website_id'); +} + +$installer->endSetup(); diff --git a/app/code/core/Maho/Giftcard/sql/schema.php b/app/code/core/Maho/Giftcard/sql/schema.php index 9bc56e8951..4313ba4c5a 100644 --- a/app/code/core/Maho/Giftcard/sql/schema.php +++ b/app/code/core/Maho/Giftcard/sql/schema.php @@ -17,7 +17,6 @@ $giftcard->addColumn('giftcard_id', Types::INTEGER, ['unsigned' => true, 'autoincrement' => true]); $giftcard->addColumn('code', Types::STRING, ['length' => 64]); $giftcard->addColumn('status', Types::STRING, ['length' => 32, 'default' => 'active']); - $giftcard->addColumn('website_id', Types::SMALLINT, ['unsigned' => true]); $giftcard->addColumn('balance', Types::DECIMAL, ['precision' => 12, 'scale' => 4, 'default' => '0.0000']); $giftcard->addColumn('initial_balance', Types::DECIMAL, ['precision' => 12, 'scale' => 4, 'default' => '0.0000']); $giftcard->addColumn('recipient_name', Types::STRING, ['length' => 255, 'notnull' => false]); @@ -36,17 +35,10 @@ PrimaryKeyConstraint::editor()->setUnquotedColumnNames('giftcard_id')->create(), ); $giftcard->addUniqueIndex(['code']); - $giftcard->addIndex(['website_id']); $giftcard->addIndex(['status']); $giftcard->addIndex(['status', 'expires_at']); $giftcard->addIndex(['purchase_order_id']); $giftcard->addIndex(['email_scheduled_at', 'email_sent_at']); - $giftcard->addForeignKeyConstraint( - 'core_website', - ['website_id'], - ['website_id'], - ['onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE'], - ); $giftcard->addForeignKeyConstraint( 'sales_flat_order', ['purchase_order_id'], @@ -134,4 +126,28 @@ 'precision' => 12, 'scale' => 4, 'notnull' => false, 'default' => '0.0000', 'comment' => 'Base Gift Card Amount', ]); + + // Junction table for multi-website gift card associations (1.1.0+). + // A card can be valid on any subset of websites; apply-time validation is + // a membership check against these rows. + $website = $schema->createTable('giftcard_website'); + $website->addColumn('giftcard_id', Types::INTEGER, ['unsigned' => true]); + $website->addColumn('website_id', Types::SMALLINT, ['unsigned' => true]); + $website->addPrimaryKeyConstraint( + PrimaryKeyConstraint::editor()->setUnquotedColumnNames('giftcard_id', 'website_id')->create(), + ); + $website->addIndex(['website_id']); + $website->addForeignKeyConstraint( + 'giftcard', + ['giftcard_id'], + ['giftcard_id'], + ['onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE'], + ); + $website->addForeignKeyConstraint( + 'core_website', + ['website_id'], + ['website_id'], + ['onUpdate' => 'CASCADE', 'onDelete' => 'CASCADE'], + ); + $website->setComment('Gift Card to Website Associations'); }; diff --git a/app/design/adminhtml/default/default/layout/giftcard.xml b/app/design/adminhtml/default/default/layout/giftcard.xml index 2f908ce014..cdab35192a 100644 --- a/app/design/adminhtml/default/default/layout/giftcard.xml +++ b/app/design/adminhtml/default/default/layout/giftcard.xml @@ -11,6 +11,28 @@ SPDX-License-Identifier: OSL-3.0 + + + + + + + + + + + + + generalgiftcard_edit_tab_form + historygiftcard_edit_tab_history + + + + diff --git a/app/design/frontend/base/default/layout/giftcard.xml b/app/design/frontend/base/default/layout/giftcard.xml index d0239ef995..5424323d71 100644 --- a/app/design/frontend/base/default/layout/giftcard.xml +++ b/app/design/frontend/base/default/layout/giftcard.xml @@ -4,6 +4,25 @@ SPDX-FileCopyrightText: 2025-2026 Maho SPDX-License-Identifier: OSL-3.0 --> + + + + + giftcard_balance + giftcard/balance + + + + + + + + + + + + + diff --git a/app/design/frontend/base/default/template/giftcard/customer/balance.phtml b/app/design/frontend/base/default/template/giftcard/customer/balance.phtml new file mode 100644 index 0000000000..713d07dedb --- /dev/null +++ b/app/design/frontend/base/default/template/giftcard/customer/balance.phtml @@ -0,0 +1,70 @@ + + * SPDX-License-Identifier: AFL-3.0 + * @package Maho_Giftcard + * + * @var Maho_Giftcard_Block_Customer_Balance $this + */ +$_result = $this->getLastLookup(); +?> +
+

__('Check Gift Card Balance') ?>

+
+ +getMessagesBlock()->getGroupedHtml() ?> + + + + + + + + diff --git a/app/locale/en_US/Maho_Giftcard.csv b/app/locale/en_US/Maho_Giftcard.csv index a9fd1d8035..f0135b2657 100644 --- a/app/locale/en_US/Maho_Giftcard.csv +++ b/app/locale/en_US/Maho_Giftcard.csv @@ -1,3 +1,4 @@ +"* Required Fields","* Required Fields" "-- Please Select --","-- Please Select --" "0 = never expires. Can be overridden per product.","0 = never expires. Can be overridden per product." "Action","Action" @@ -5,6 +6,8 @@ "Add a message...","Add a message..." "Add New Gift Card","Add New Gift Card" "Admin Comment","Admin Comment" +"A gift card can only be assigned to websites that share the same base currency.","A gift card can only be assigned to websites that share the same base currency." +"A gift card must be associated with at least one website.","A gift card must be associated with at least one website." "Allow Gift Message","Allow Gift Message" "Allow Message","Allow Message" "All Websites","All Websites" @@ -15,16 +18,19 @@ "Apply","Apply" "Apply Gift Card","Apply Gift Card" "Applying...","Applying..." +"Back to My Account","Back to My Account" "Balance","Balance" "Balance: %s","Balance: %s" "Balance After","Balance After" "Balance Before","Balance Before" +"Balance for %s","Balance for %s" "Can be overridden per product.","Can be overridden per product." "Cannot apply gift card.","Cannot apply gift card." "Cannot remove gift card.","Cannot remove gift card." "Card Type","Card Type" "Check","Check" "Check Balance","Check Balance" +"Check Gift Card Balance","Check Gift Card Balance" "Checking...","Checking..." "Code","Code" "Code Length","Code Length" @@ -35,7 +41,9 @@ "Created At","Created At" "Create Gift Card Products","Create Gift Card Products" "Current Balance","Current Balance" +"Current balance:","Current balance:" "Custom Amount","Custom Amount" +"Customer Gift Card Balance","Customer Gift Card Balance" "Date","Date" "Default: %s","Default: %s" "Default Expiry (days)","Default Expiry (days)" @@ -50,15 +58,19 @@ "Email Template","Email Template" "Enable Gift Cards","Enable Gift Cards" "Enter gift card code","Enter gift card code" +"Enter the code printed on your gift card to view the current balance and expiry date.","Enter the code printed on your gift card to view the current balance and expiry date." +"Enter your gift card code","Enter your gift card code" "Error generating gift card PDF: %s","Error generating gift card PDF: %s" "Expired","Expired" "Expires","Expires" +"Expires:","Expires:" "Expires At","Expires At" "Fixed + Range","Fixed + Range" "Fixed Amount(s)","Fixed Amount(s)" "Fixed Amounts","Fixed Amounts" "From","From" "From:","From:" +"General","General" "Gift Card","Gift Card" "Gift card ""%s"" has been fully used.","Gift card ""%s"" has been fully used." "Gift card ""%s"" has expired.","Gift card ""%s"" has expired." @@ -69,6 +81,7 @@ "Gift card ""%s"" was applied.","Gift card ""%s"" was applied." "Gift card amount cannot be less than %s","Gift card amount cannot be less than %s" "Gift card amount cannot be more than %s","Gift card amount cannot be more than %s" +"Gift Card Balance","Gift Card Balance" "Gift Card Code","Gift Card Code" "Gift Card History","Gift Card History" "Gift Card Information","Gift Card Information" @@ -82,6 +95,7 @@ "Gift Card Transaction History","Gift Card Transaction History" "Gift card was removed.","Gift card was removed." "Gift Certificates","Gift Certificates" +"Hold Ctrl/Cmd to select multiple. All selected websites must share the same base currency; the card balance is denominated in it.","Hold Ctrl/Cmd to select multiple. All selected websites must share the same base currency; the card balance is denominated in it." "Inactive","Inactive" "Initial Balance","Initial Balance" "Invalid form key. Please refresh the page and try again.","Invalid form key. Please refresh the page and try again." @@ -128,7 +142,9 @@ "This is a required field.","This is a required field." "To","To" "Too many attempts. Please wait a moment and try again.","Too many attempts. Please wait a moment and try again." +"Too many recent lookup attempts. Please wait a while before trying again.","Too many recent lookup attempts. Please wait a while before trying again." "Total: %s","Total: %s" +"Transaction History","Transaction History" "Unable to check balance.","Unable to check balance." "Unable to find a gift card to delete.","Unable to find a gift card to delete." "Unknown","Unknown" @@ -137,6 +153,8 @@ "Use Default (%s)","Use Default (%s)" "View","View" "Website","Website" +"Websites","Websites" +"We could not find an active gift card for that code on this store.","We could not find an active gift card for that code on this store." "When refunding to an expired gift card, extend expiration by this many days. 0 = no extension (card stays expired but balance is restored).","When refunding to an expired gift card, extend expiration by this many days. 0 = no extension (card stays expired but balance is restored)." "Your Email","Your Email" "Your Name","Your Name" diff --git a/tests/Api/V2/Write/GiftCardAdminTest.php b/tests/Api/V2/Write/GiftCardAdminTest.php index cb9a989b88..54ceabb07b 100644 --- a/tests/Api/V2/Write/GiftCardAdminTest.php +++ b/tests/Api/V2/Write/GiftCardAdminTest.php @@ -11,7 +11,7 @@ /** * API v2 Gift Card admin surface tests * - * Admin/service reads expose websiteId and history, status and balance are + * Admin/service reads expose websiteIds and history, status and balance are * writable via the gated REST Put (balance changes record a history entry), * while the public balance check stays masked and read-only. * @@ -39,20 +39,20 @@ function trackAdminGiftCard(string $code): void describe('GET /api/rest/v2/giftcards/{id} (admin read)', function (): void { - it('exposes websiteId, lifecycle timestamps and history', function (): void { + it('exposes websiteIds, lifecycle timestamps and history', function (): void { $create = apiPost('/api/rest/v2/giftcards', [ 'initialBalance' => 30.0, - 'websiteId' => 1, + 'websiteIds' => [1], ], adminToken()); expect($create['status'])->toBeSuccessful(); - expect($create['json']['websiteId'])->toBe(1); + expect($create['json']['websiteIds'])->toBe([1]); $id = (int) $create['json']['id']; trackAdminGiftCard($create['json']['code']); $read = apiGet("/api/rest/v2/giftcards/{$id}", adminToken()); expect($read['status'])->toBe(200); - expect($read['json']['websiteId'])->toBe(1); + expect($read['json']['websiteIds'])->toBe([1]); expect($read['json']['createdAt'])->not->toBeEmpty(); expect($read['json']['updatedAt'])->not->toBeEmpty(); expect($read['json']['history'])->toBeArray(); @@ -92,7 +92,7 @@ function trackAdminGiftCard(string $code): void it('rejects an unknown websiteId on create', function (): void { $response = apiPost('/api/rest/v2/giftcards', [ 'initialBalance' => 30.0, - 'websiteId' => 99999, + 'websiteIds' => [99999], ], adminToken()); expect($response['status'])->toBe(400); diff --git a/tests/Api/V2/Write/StoreRestrictionTest.php b/tests/Api/V2/Write/StoreRestrictionTest.php index 896b94e0ab..7ad1c36d59 100644 --- a/tests/Api/V2/Write/StoreRestrictionTest.php +++ b/tests/Api/V2/Write/StoreRestrictionTest.php @@ -367,21 +367,21 @@ function restrictMembers(array $response): array describe('Website-restricted gift card access', function (): void { - it('defaults an omitted websiteId to the current store website on REST create', function (): void { + it('defaults omitted websiteIds to the current store website on REST create', function (): void { $create = apiPost('/api/rest/v2/giftcards', ['initialBalance' => 10.0], adminToken()); expect($create['status'])->toBeSuccessful(); trackRestrictGiftCard($create['json']['code']); - expect($create['json']['websiteId'])->toBe(1); + expect($create['json']['websiteIds'])->toBe([1]); }); - it('defaults an omitted websiteId to the current store website on GraphQL create', function (): void { + it('defaults omitted websiteIds to the current store website on GraphQL create', function (): void { $mutation = <<<'GRAPHQL' mutation { createGiftCard(input: { initialBalance: 10 }) { giftCard { code - websiteId + websiteIds } } } @@ -392,7 +392,7 @@ function restrictMembers(array $response): array $card = $response['json']['data']['createGiftCard']['giftCard']; trackRestrictGiftCard($card['code']); - expect($card['websiteId'])->toBe(1); + expect($card['websiteIds'])->toBe([1]); }); it('hides gift cards of other websites from restricted tokens and the public balance check', function (): void { @@ -400,7 +400,7 @@ function restrictMembers(array $response): array $create = apiPost('/api/rest/v2/giftcards', [ 'initialBalance' => 25.0, 'code' => $code, - 'websiteId' => restrictWebsiteId(), + 'websiteIds' => [restrictWebsiteId()], ], adminToken()); expect($create['status'])->toBeSuccessful(); trackRestrictGiftCard($code); @@ -420,7 +420,7 @@ function restrictMembers(array $response): array $restricted = serviceToken(['giftcards/create', 'giftcards/write', 'giftcards/read'], [1]); $response = apiPost('/api/rest/v2/giftcards', [ 'initialBalance' => 10.0, - 'websiteId' => restrictWebsiteId(), + 'websiteIds' => [restrictWebsiteId()], ], $restricted); expect($response['status'])->toBe(403); diff --git a/tests/Backend/Integration/ApiPlatform/public-api-fields.php b/tests/Backend/Integration/ApiPlatform/public-api-fields.php index 3e6ef4e8f1..287438f8a9 100644 --- a/tests/Backend/Integration/ApiPlatform/public-api-fields.php +++ b/tests/Backend/Integration/ApiPlatform/public-api-fields.php @@ -117,7 +117,7 @@ 'balance', 'code', 'createdAt', 'currencyCode', 'emailScheduledAt', 'emailSentAt', 'expiresAt', 'extensions', 'history', 'id', 'initialBalance', 'message', 'purchaseOrderId', 'purchaseOrderItemId', 'recipientEmail', 'recipientName', 'senderEmail', 'senderName', - 'status', 'updatedAt', 'websiteId', + 'status', 'updatedAt', 'websiteIds', ], 'GroupedProductLink' => [ 'childProductId', 'childProductName', 'childProductSku', 'extensions', 'id', 'position', 'qty', diff --git a/tests/Backend/Integration/Checkout/Api/CartCurrencyConsistencyTest.php b/tests/Backend/Integration/Checkout/Api/CartCurrencyConsistencyTest.php index eec5b0065c..9516453265 100644 --- a/tests/Backend/Integration/Checkout/Api/CartCurrencyConsistencyTest.php +++ b/tests/Backend/Integration/Checkout/Api/CartCurrencyConsistencyTest.php @@ -117,7 +117,7 @@ function createEurQuoteWithProduct(Mage_Catalog_Model_Product $product, int $qty $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode(Mage::helper('giftcard')->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(50.00); $giftcard->setInitialBalance(50.00); $giftcard->save(); diff --git a/tests/Backend/Integration/Giftcard/Helper/EmailTest.php b/tests/Backend/Integration/Giftcard/Helper/EmailTest.php index ade38e01b0..863a028a09 100644 --- a/tests/Backend/Integration/Giftcard/Helper/EmailTest.php +++ b/tests/Backend/Integration/Giftcard/Helper/EmailTest.php @@ -18,7 +18,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setRecipientEmail(null); // No email @@ -35,7 +35,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setRecipientEmail('test@example.com'); @@ -64,7 +64,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(50.00); $giftcard->setInitialBalance(50.00); $giftcard->setRecipientEmail('schedule@test.com'); @@ -89,7 +89,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(50.00); $giftcard->setInitialBalance(50.00); $giftcard->setRecipientEmail(null); @@ -126,7 +126,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setRecipientEmail('queue-test@example.com'); @@ -147,7 +147,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode('TEST-EMAIL-VARS'); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(150.00); $giftcard->setInitialBalance(150.00); $giftcard->setRecipientEmail('vars-test@example.com'); @@ -207,7 +207,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setRecipientEmail('sent-test@example.com'); @@ -232,7 +232,7 @@ $sentCard = Mage::getModel('giftcard/giftcard'); $sentCard->setCode($this->helper->generateCode()); $sentCard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $sentCard->setWebsiteId(1); + $sentCard->setWebsiteIds([1]); $sentCard->setBalance(50.00); $sentCard->setInitialBalance(50.00); $sentCard->setRecipientEmail('sent@example.com'); @@ -242,7 +242,7 @@ $unsentCard = Mage::getModel('giftcard/giftcard'); $unsentCard->setCode($this->helper->generateCode()); $unsentCard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $unsentCard->setWebsiteId(1); + $unsentCard->setWebsiteIds([1]); $unsentCard->setBalance(50.00); $unsentCard->setInitialBalance(50.00); $unsentCard->setRecipientEmail('unsent@example.com'); diff --git a/tests/Backend/Integration/Giftcard/InvalidCardRemovalTest.php b/tests/Backend/Integration/Giftcard/InvalidCardRemovalTest.php index 50705a1923..78b4e92dc4 100644 --- a/tests/Backend/Integration/Giftcard/InvalidCardRemovalTest.php +++ b/tests/Backend/Integration/Giftcard/InvalidCardRemovalTest.php @@ -52,7 +52,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode(Mage::helper('giftcard')->generateCode()); $giftcard->setStatus($status); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(50.00); $giftcard->setInitialBalance(50.00); if ($expiresAt !== null) { diff --git a/tests/Backend/Integration/Giftcard/Model/CronTest.php b/tests/Backend/Integration/Giftcard/Model/CronTest.php index ff240f5451..19e3f60dbd 100644 --- a/tests/Backend/Integration/Giftcard/Model/CronTest.php +++ b/tests/Backend/Integration/Giftcard/Model/CronTest.php @@ -29,7 +29,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($pastDate); @@ -51,7 +51,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(50.00); $giftcard->setInitialBalance(50.00); $giftcard->setExpiresAt($pastDate); @@ -81,7 +81,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt(null); // No expiration @@ -101,7 +101,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($futureDate); @@ -121,7 +121,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_EXPIRED); // Already expired - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($pastDate); @@ -153,7 +153,7 @@ $disabledCard = Mage::getModel('giftcard/giftcard'); $disabledCard->setCode($this->helper->generateCode()); $disabledCard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_DISABLED); - $disabledCard->setWebsiteId(1); + $disabledCard->setWebsiteIds([1]); $disabledCard->setBalance(100.00); $disabledCard->setInitialBalance(100.00); $disabledCard->setExpiresAt($pastDate); @@ -163,7 +163,7 @@ $usedCard = Mage::getModel('giftcard/giftcard'); $usedCard->setCode($this->helper->generateCode()); $usedCard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_USED); - $usedCard->setWebsiteId(1); + $usedCard->setWebsiteIds([1]); $usedCard->setBalance(0.00); $usedCard->setInitialBalance(100.00); $usedCard->setExpiresAt($pastDate); @@ -193,7 +193,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setRecipientEmail('recipient@test.com'); @@ -217,7 +217,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setRecipientEmail('future@test.com'); @@ -241,7 +241,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setRecipientEmail('already-sent@test.com'); @@ -263,7 +263,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setRecipientEmail(null); // No email diff --git a/tests/Backend/Integration/Giftcard/Model/GiftcardCrudTest.php b/tests/Backend/Integration/Giftcard/Model/GiftcardCrudTest.php index 3f6206e1b3..71ba42e918 100644 --- a/tests/Backend/Integration/Giftcard/Model/GiftcardCrudTest.php +++ b/tests/Backend/Integration/Giftcard/Model/GiftcardCrudTest.php @@ -17,7 +17,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setRecipientName('Test Recipient'); @@ -39,7 +39,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(75.50); $giftcard->setInitialBalance(75.50); $giftcard->save(); @@ -59,7 +59,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(200.00); $giftcard->setInitialBalance(200.00); $giftcard->save(); @@ -83,7 +83,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setMessage('Original message'); @@ -110,7 +110,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -131,7 +131,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -161,7 +161,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(50.00); $giftcard->setInitialBalance(50.00); $giftcard->save(); @@ -180,7 +180,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(25.00); $giftcard->setInitialBalance(25.00); $giftcard->save(); @@ -196,7 +196,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_DISABLED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -212,7 +212,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -231,7 +231,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_USED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -256,7 +256,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_USED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -275,7 +275,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -300,7 +300,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -317,7 +317,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_USED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -339,7 +339,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($pastDate); @@ -363,7 +363,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($futureDate); @@ -381,7 +381,7 @@ $activeCard = Mage::getModel('giftcard/giftcard'); $activeCard->setCode($helper->generateCode()); $activeCard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $activeCard->setWebsiteId(1); + $activeCard->setWebsiteIds([1]); $activeCard->setBalance(100.00); $activeCard->setInitialBalance(100.00); $activeCard->save(); @@ -389,7 +389,7 @@ $usedCard = Mage::getModel('giftcard/giftcard'); $usedCard->setCode($helper->generateCode()); $usedCard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_USED); - $usedCard->setWebsiteId(1); + $usedCard->setWebsiteIds([1]); $usedCard->setBalance(0.00); $usedCard->setInitialBalance(50.00); $usedCard->save(); @@ -409,16 +409,24 @@ $card1 = Mage::getModel('giftcard/giftcard'); $card1->setCode($helper->generateCode()); $card1->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $card1->setWebsiteId(1); + $card1->setWebsiteIds([1]); $card1->setBalance(100.00); $card1->setInitialBalance(100.00); $card1->save(); $collection = Mage::getResourceModel('giftcard/giftcard_collection') - ->addFieldToFilter('website_id', 1) + ->addWebsiteFilter(1) ->addFieldToFilter('giftcard_id', $card1->getId()); expect($collection->getSize())->toBe(1); + + // Membership filtering: the same card is not returned for a website + // it is not associated with. + $otherWebsite = Mage::getResourceModel('giftcard/giftcard_collection') + ->addWebsiteFilter(999) + ->addFieldToFilter('giftcard_id', $card1->getId()); + + expect($otherWebsite->getSize())->toBe(0); }); }); @@ -430,7 +438,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -462,7 +470,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($code); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); diff --git a/tests/Backend/Integration/Giftcard/Model/MultistoreMulticurrencyTest.php b/tests/Backend/Integration/Giftcard/Model/MultistoreMulticurrencyTest.php index 4b92e39669..c29ba07908 100644 --- a/tests/Backend/Integration/Giftcard/Model/MultistoreMulticurrencyTest.php +++ b/tests/Backend/Integration/Giftcard/Model/MultistoreMulticurrencyTest.php @@ -48,7 +48,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it $this->cardWebsite1 = Mage::getModel('giftcard/giftcard'); $this->cardWebsite1->setCode($this->helper->generateCode()); $this->cardWebsite1->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->cardWebsite1->setWebsiteId(1); + $this->cardWebsite1->setWebsiteIds([1]); $this->cardWebsite1->setBalance(100.00); $this->cardWebsite1->setInitialBalance(100.00); $this->cardWebsite1->save(); @@ -57,7 +57,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it $this->cardWebsite2 = Mage::getModel('giftcard/giftcard'); $this->cardWebsite2->setCode($this->helper->generateCode()); $this->cardWebsite2->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->cardWebsite2->setWebsiteId($this->testWebsite->getId()); + $this->cardWebsite2->setWebsiteIds([$this->testWebsite->getId()]); $this->cardWebsite2->setBalance(75.00); $this->cardWebsite2->setInitialBalance(75.00); $this->cardWebsite2->save(); @@ -191,7 +191,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(100.00); // Stored in website's base currency $this->giftcard->setInitialBalance(100.00); $this->giftcard->save(); @@ -228,7 +228,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it }); test('gift card getCurrencyCode returns website base currency', function () { - $website = Mage::app()->getWebsite($this->giftcard->getWebsiteId()); + $website = $this->giftcard->getWebsite(); $expectedCurrency = $website->getBaseCurrencyCode(); expect($this->giftcard->getCurrencyCode())->toBe($expectedCurrency); @@ -243,7 +243,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(100.00); // In base currency $this->giftcard->setInitialBalance(100.00); $this->giftcard->save(); @@ -335,7 +335,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(50.00); $this->giftcard->setInitialBalance(50.00); $this->giftcard->save(); @@ -430,7 +430,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($pastDate); @@ -445,7 +445,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_DISABLED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -458,7 +458,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -470,7 +470,7 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); // Keep FK valid + $giftcard->setWebsiteIds([1]); // Keep FK valid $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -479,9 +479,9 @@ function setQuoteAddressItems(Mage_Sales_Model_Quote_Address $address, array $it expect($giftcard->isValidForWebsite(1))->toBeTrue(); // Test the core validation logic by checking a hypothetical mismatch - // The isValidForWebsite method checks: isValid() && (websiteId === $websiteId) - // We verify the website matching logic works correctly + // The isValidForWebsite method checks: isValid() && junction membership + // We verify the website association is persisted correctly expect($giftcard->isValid())->toBeTrue(); - expect((int) $giftcard->getWebsiteId())->toBe(1); + expect($giftcard->getWebsiteIds())->toBe([1]); }); }); diff --git a/tests/Backend/Integration/Giftcard/Model/Total/QuoteTest.php b/tests/Backend/Integration/Giftcard/Model/Total/QuoteTest.php index 280469c806..0ed7a3ab92 100644 --- a/tests/Backend/Integration/Giftcard/Model/Total/QuoteTest.php +++ b/tests/Backend/Integration/Giftcard/Model/Total/QuoteTest.php @@ -42,7 +42,7 @@ $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(100.00); $this->giftcard->setInitialBalance(100.00); $this->giftcard->save(); @@ -84,7 +84,7 @@ $smallCard = Mage::getModel('giftcard/giftcard'); $smallCard->setCode($this->helper->generateCode()); $smallCard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $smallCard->setWebsiteId(1); + $smallCard->setWebsiteIds([1]); $smallCard->setBalance(5.00); // Very small balance $smallCard->setInitialBalance(5.00); $smallCard->save(); @@ -117,7 +117,7 @@ $card2 = Mage::getModel('giftcard/giftcard'); $card2->setCode($this->helper->generateCode()); $card2->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $card2->setWebsiteId(1); + $card2->setWebsiteIds([1]); $card2->setBalance(50.00); $card2->setInitialBalance(50.00); $card2->save(); @@ -153,7 +153,7 @@ $bigCard = Mage::getModel('giftcard/giftcard'); $bigCard->setCode($this->helper->generateCode()); $bigCard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $bigCard->setWebsiteId(1); + $bigCard->setWebsiteIds([1]); $bigCard->setBalance(10000.00); // Much more than any product $bigCard->setInitialBalance(10000.00); $bigCard->save(); @@ -186,7 +186,7 @@ $invalidCard = Mage::getModel('giftcard/giftcard'); $invalidCard->setCode($this->helper->generateCode()); $invalidCard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_DISABLED); - $invalidCard->setWebsiteId(1); + $invalidCard->setWebsiteIds([1]); $invalidCard->setBalance(100.00); $invalidCard->setInitialBalance(100.00); $invalidCard->save(); @@ -256,7 +256,7 @@ $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(100.00); $this->giftcard->setInitialBalance(100.00); $this->giftcard->save(); @@ -325,7 +325,7 @@ $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(100.00); $this->giftcard->setInitialBalance(100.00); $this->giftcard->save(); @@ -402,7 +402,7 @@ $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(500.00); $this->giftcard->setInitialBalance(500.00); $this->giftcard->save(); diff --git a/tests/Backend/Integration/Giftcard/Model/WebsiteAssociationTest.php b/tests/Backend/Integration/Giftcard/Model/WebsiteAssociationTest.php new file mode 100644 index 0000000000..4823efe625 --- /dev/null +++ b/tests/Backend/Integration/Giftcard/Model/WebsiteAssociationTest.php @@ -0,0 +1,178 @@ + + * SPDX-License-Identifier: OSL-3.0 + */ + +declare(strict_types=1); + +uses(Tests\MahoBackendTestCase::class); + +function createActiveCard(): Maho_Giftcard_Model_Giftcard +{ + $card = Mage::getModel('giftcard/giftcard'); + $card->setCode(Mage::helper('giftcard')->generateCode()); + $card->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); + $card->setBalance(50.00); + $card->setInitialBalance(50.00); + return $card; +} + +describe('Giftcard Website Associations (junction)', function () { + test('setWebsiteIds persists to the junction and survives reload', function () { + $card = createActiveCard(); + $card->setWebsiteIds([1]); + $card->save(); + + $reloaded = Mage::getModel('giftcard/giftcard')->load($card->getId()); + expect($reloaded->getWebsiteIds())->toBe([1]); + + $card->delete(); + }); + + test('a new card saved without explicit associations defaults to the current website', function () { + $card = createActiveCard(); + $card->save(); + + $reloaded = Mage::getModel('giftcard/giftcard')->load($card->getId()); + expect($reloaded->getWebsiteIds())->toBe([(int) Mage::app()->getStore()->getWebsiteId()]); + + $card->delete(); + }); + + test('membership validation follows the junction', function () { + $card = createActiveCard(); + $card->setWebsiteIds([1]); + $card->save(); + + $reloaded = Mage::getModel('giftcard/giftcard')->load($card->getId()); + expect($reloaded->isValidForWebsite(1))->toBeTrue(); + expect($reloaded->isValidForWebsite(999))->toBeFalse(); + + $card->delete(); + }); + + test('an explicit empty website set is rejected', function () { + $card = createActiveCard(); + $card->setData('website_ids', []); + + expect(fn() => $card->save())->toThrow( + Mage_Core_Exception::class, + 'A gift card must be associated with at least one website.', + ); + }); + + test('a save that did not touch associations leaves the junction alone', function () { + $card = createActiveCard(); + $card->setWebsiteIds([1]); + $card->save(); + + $reloaded = Mage::getModel('giftcard/giftcard')->load($card->getId()); + // Reading associations then saving an unrelated change must not + // count as "associations were set" (the hot checkout path does this). + expect($reloaded->getWebsiteIds())->toBe([1]); + $reloaded->setBalance(25.00); + $reloaded->save(); + + $again = Mage::getModel('giftcard/giftcard')->load($card->getId()); + expect($again->getWebsiteIds())->toBe([1]); + expect((float) $again->getBalance())->toBe(25.00); + + $card->delete(); + }); + + test('websites with different base currencies cannot be associated to one card', function () { + $website = Mage::getModel('core/website'); + $website->setCode('gc_currency_test_' . uniqid()); + $website->setName('Giftcard Currency Test Website'); + $website->save(); + + try { + // Give the new website its own base currency, distinct from the + // default website's (which resolves globally, e.g. USD). + $code = $website->getCode(); + $baseCurrency = Mage::app()->getBaseCurrencyCode(); + $otherCurrency = $baseCurrency === 'EUR' ? 'USD' : 'EUR'; + Mage::getConfig()->setNode( + "websites/{$code}/catalog/price/scope", + (string) Mage_Core_Model_Store::PRICE_SCOPE_WEBSITE, + ); + Mage::getConfig()->setNode("websites/{$code}/currency/options/base", $otherCurrency); + + $card = createActiveCard(); + $card->setWebsiteIds([1, (int) $website->getId()]); + + expect(fn() => $card->save())->toThrow( + Mage_Core_Exception::class, + 'A gift card can only be assigned to websites that share the same base currency.', + ); + } finally { + $website->delete(); + } + }); +}); + +describe('Giftcard 1.0.0 -> 1.1.0 website migration', function () { + afterEach(function () { + // If an assertion failed mid-test, don't leak the legacy column into + // the shared test database. + $setup = new Mage_Core_Model_Resource_Setup('giftcard_setup'); + $connection = $setup->getConnection(); + $giftcardTable = $setup->getTable('giftcard/giftcard'); + if ($connection->tableColumnExists($giftcardTable, 'website_id')) { + $connection->dropColumn($giftcardTable, 'website_id'); + } + }); + + test('backfills the junction from the legacy column and drops it', function () { + $setup = new Mage_Core_Model_Resource_Setup('giftcard_setup'); + $connection = $setup->getConnection(); + $giftcardTable = $setup->getTable('giftcard/giftcard'); + $junctionTable = $setup->getTable('giftcard/website'); + $script = Mage::getModuleDir('sql', 'Maho_Giftcard') . '/giftcard_setup/upgrade-1.0.0-1.1.0.php'; + $runScript = function () use ($setup, $script): void { + (function (string $file): void { + include $file; + })->call($setup, $script); + }; + + // Recreate the pre-1.1.0 shape: the scalar column, and a card that + // only exists there (no junction rows). + $connection->addColumn($giftcardTable, 'website_id', [ + 'type' => Maho\Db\Ddl\Table::TYPE_SMALLINT, + 'unsigned' => true, + 'nullable' => true, + 'comment' => 'Legacy single-website association (test fixture)', + ]); + $now = Mage::app()->getLocale()->formatDateForDb('now'); + $connection->insert($giftcardTable, [ + 'code' => 'MIGRATION-TEST-' . strtoupper(uniqid()), + 'status' => Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE, + 'website_id' => 1, + 'balance' => 42.0, + 'initial_balance' => 42.0, + 'created_at' => $now, + 'updated_at' => $now, + ]); + $giftcardId = (int) $connection->lastInsertId($giftcardTable, 'giftcard_id'); + $connection->delete($junctionTable, ['giftcard_id = ?' => $giftcardId]); + + try { + $runScript(); + + // Conservative backfill: exactly the card's original website. + $select = $connection->select() + ->from($junctionTable, ['website_id']) + ->where('giftcard_id = ?', $giftcardId); + expect(array_map('intval', $connection->fetchCol($select)))->toBe([1]); + + // The legacy column is gone, and a re-run is a clean no-op. + expect($connection->tableColumnExists($giftcardTable, 'website_id'))->toBeFalse(); + $runScript(); + expect(array_map('intval', $connection->fetchCol($select)))->toBe([1]); + } finally { + $connection->delete($giftcardTable, ['giftcard_id = ?' => $giftcardId]); + } + }); +}); diff --git a/tests/Backend/Integration/Giftcard/Observer/OrderFlowTest.php b/tests/Backend/Integration/Giftcard/Observer/OrderFlowTest.php index 173f5b957c..aa00801eac 100644 --- a/tests/Backend/Integration/Giftcard/Observer/OrderFlowTest.php +++ b/tests/Backend/Integration/Giftcard/Observer/OrderFlowTest.php @@ -124,7 +124,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -261,7 +261,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(99999.00); $giftcard->setInitialBalance(99999.00); $giftcard->save(); @@ -376,7 +376,7 @@ $giftcard1 = Mage::getModel('giftcard/giftcard'); $giftcard1->setCode($this->helper->generateCode()); $giftcard1->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard1->setWebsiteId(1); + $giftcard1->setWebsiteIds([1]); $giftcard1->setBalance(60.00); $giftcard1->setInitialBalance(60.00); $giftcard1->save(); @@ -384,7 +384,7 @@ $giftcard2 = Mage::getModel('giftcard/giftcard'); $giftcard2->setCode($this->helper->generateCode()); $giftcard2->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard2->setWebsiteId(1); + $giftcard2->setWebsiteIds([1]); $giftcard2->setBalance(40.00); $giftcard2->setInitialBalance(40.00); $giftcard2->save(); @@ -713,7 +713,7 @@ $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(100.00); $this->giftcard->setInitialBalance(100.00); $this->giftcard->save(); @@ -769,23 +769,22 @@ $websiteId = (int) $quote->getStore()->getWebsiteId(); - // Card's website - $cardWebsiteId = (int) $this->giftcard->getWebsiteId(); + // Card's website associations + $cardWebsiteIds = $this->giftcard->getWebsiteIds(); - // For this test, they should match - expect($websiteId)->toBe($cardWebsiteId); + // For this test, the quote's website should be associated + expect($cardWebsiteIds)->toContain($websiteId); - // Test the isValidForWebsite method with a different website ID + // Test the isValidForWebsite method with a different website set // without actually saving to avoid foreign key constraint - // We set the data without persisting - $this->giftcard->setData('website_id', 999); + $this->giftcard->setWebsiteIds([999]); - // The isValidForWebsite method should return false because card's website (999) - // doesn't match the quote's website (1) + // The isValidForWebsite method should return false because the quote's + // website (1) is not in the card's association set (999) expect($this->giftcard->isValidForWebsite($websiteId))->toBeFalse(); // Also verify a valid website ID would pass - $this->giftcard->setData('website_id', $websiteId); + $this->giftcard->setWebsiteIds([$websiteId]); expect($this->giftcard->isValidForWebsite($websiteId))->toBeTrue(); }); }); @@ -798,7 +797,7 @@ $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->save(); $code = $giftcard->getCode(); @@ -863,7 +862,7 @@ $giftcard1->setBalance(100.00); $giftcard1->setInitialBalance(100.00); $giftcard1->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard1->setWebsiteId(1); + $giftcard1->setWebsiteIds([1]); $giftcard1->save(); $giftcard2 = Mage::getModel('giftcard/giftcard'); @@ -871,7 +870,7 @@ $giftcard2->setBalance(75.00); $giftcard2->setInitialBalance(75.00); $giftcard2->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard2->setWebsiteId(1); + $giftcard2->setWebsiteIds([1]); $giftcard2->save(); $code1 = $giftcard1->getCode(); @@ -953,7 +952,7 @@ $giftcard->setBalance(100.00); $giftcard->setInitialBalance(100.00); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setExpiresAt($expiresAt->format(Mage_Core_Model_Locale::DATETIME_FORMAT)); $giftcard->save(); diff --git a/tests/Backend/Integration/Giftcard/Observer/RefundFlowTest.php b/tests/Backend/Integration/Giftcard/Observer/RefundFlowTest.php index 49862a7e71..171247ee2c 100644 --- a/tests/Backend/Integration/Giftcard/Observer/RefundFlowTest.php +++ b/tests/Backend/Integration/Giftcard/Observer/RefundFlowTest.php @@ -18,7 +18,7 @@ $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(100.00); $this->giftcard->setInitialBalance(100.00); $this->giftcard->save(); @@ -169,7 +169,7 @@ $this->giftcard1 = Mage::getModel('giftcard/giftcard'); $this->giftcard1->setCode($this->helper->generateCode()); $this->giftcard1->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard1->setWebsiteId(1); + $this->giftcard1->setWebsiteIds([1]); $this->giftcard1->setBalance(30.00); $this->giftcard1->setInitialBalance(30.00); $this->giftcard1->save(); @@ -177,7 +177,7 @@ $this->giftcard2 = Mage::getModel('giftcard/giftcard'); $this->giftcard2->setCode($this->helper->generateCode()); $this->giftcard2->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard2->setWebsiteId(1); + $this->giftcard2->setWebsiteIds([1]); $this->giftcard2->setBalance(70.00); $this->giftcard2->setInitialBalance(70.00); $this->giftcard2->save(); @@ -314,7 +314,7 @@ $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(100.00); $this->giftcard->setInitialBalance(100.00); $this->giftcard->save(); @@ -456,7 +456,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_USED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(50.00); $giftcard->save(); @@ -521,7 +521,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(50.00); $giftcard->setInitialBalance(50.00); $giftcard->save(); @@ -562,7 +562,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(40.00); // $40 remaining $giftcard->setInitialBalance(100.00); $giftcard->save(); @@ -605,7 +605,7 @@ $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setCode($this->helper->generateCode()); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); $this->giftcard->setBalance(150.00); $this->giftcard->setInitialBalance(150.00); $this->giftcard->save(); @@ -692,7 +692,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_EXPIRED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($pastDate->format(Mage_Core_Model_Locale::DATETIME_FORMAT)); @@ -745,7 +745,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); // Not yet marked expired - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(20.00); // Still has some balance $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($pastDate->format(Mage_Core_Model_Locale::DATETIME_FORMAT)); @@ -795,7 +795,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_USED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($soonDate->format(Mage_Core_Model_Locale::DATETIME_FORMAT)); @@ -845,7 +845,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_USED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(100.00); $giftcard->setExpiresAt($futureDate->format(Mage_Core_Model_Locale::DATETIME_FORMAT)); @@ -890,7 +890,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_EXPIRED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(50.00); $giftcard->setExpiresAt($pastDate->format(Mage_Core_Model_Locale::DATETIME_FORMAT)); @@ -933,7 +933,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_USED); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance(0.00); $giftcard->setInitialBalance(75.00); $giftcard->setExpiresAt(null); // No expiration diff --git a/tests/Backend/Integration/Giftcard/PartialAmountTest.php b/tests/Backend/Integration/Giftcard/PartialAmountTest.php index 4b3a42375b..d736da76f1 100644 --- a/tests/Backend/Integration/Giftcard/PartialAmountTest.php +++ b/tests/Backend/Integration/Giftcard/PartialAmountTest.php @@ -43,7 +43,7 @@ $giftcard = Mage::getModel('giftcard/giftcard'); $giftcard->setCode($this->helper->generateCode()); $giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); - $giftcard->setWebsiteId(1); + $giftcard->setWebsiteIds([1]); $giftcard->setBalance($cardBalance); $giftcard->setInitialBalance($cardBalance); $giftcard->save(); diff --git a/tests/Backend/Unit/Giftcard/Model/GiftcardTest.php b/tests/Backend/Unit/Giftcard/Model/GiftcardTest.php index e9839cee20..aba3ff3307 100644 --- a/tests/Backend/Unit/Giftcard/Model/GiftcardTest.php +++ b/tests/Backend/Unit/Giftcard/Model/GiftcardTest.php @@ -46,7 +46,7 @@ describe('Giftcard isValid() Logic', function () { beforeEach(function () { $this->giftcard = Mage::getModel('giftcard/giftcard'); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); }); test('returns false when status is not active', function () { @@ -91,7 +91,7 @@ $this->giftcard = Mage::getModel('giftcard/giftcard'); $this->giftcard->setStatus(Maho_Giftcard_Model_Giftcard::STATUS_ACTIVE); $this->giftcard->setBalance(100.00); - $this->giftcard->setWebsiteId(1); + $this->giftcard->setWebsiteIds([1]); }); test('returns true for matching website', function () {