Skip to content

Gift Card Management (Admin) ​

Flow ID: AD-23 Module(s): gift_cards, coupons, eshop (gifts) Complexity: Medium-High Last Updated: 2026-09-14

Business Context ​

Gift cards are monetary vouchers purchased by customers through a dedicated checkout flow (separate from the shopping cart). On successful payment, the system generates a single-use coupon code and delivers it to the recipient via email and/or SMS. Admins manage two distinct subsystems under "gift cards": (1) gift card orders -- purchasable monetary vouchers that convert to coupon codes, and (2) promotional gifts -- free products added to orders based on configurable rules. This document covers both admin surfaces.


1. Gift Card Order Management ​

1.1 Admin Listing ​

Controller: ecommercen/gift_cards/controllers/AdvGiftCardAdminListing.php (254 lines) Auth: ADVISABLE, ADMIN, ORDERS Route: admin/giftCards (mapped via application/config/routes.php) Admin Menu: ORDERS group, icon admin.menu.gift_cards.listing.icon

The listing page is a Vue-powered SPA that fetches data via JSON endpoints on the same controller.

Endpoints (internal JSON API via ApiEndpointTrait):

MethodActionDescription
list()GET with query paramsPaginated, filtered list (50 per page)
get($orderId)GETSingle gift card order detail
cancelGiftCard($orderId)POSTCancel a gift card order
acceptGiftCard($orderId)POSTAccept/complete a gift card order; refuses an already-Completed order with 409 Conflict (see §1.3)
scheduleEmailJob($orderId)POSTQueue email resend job
scheduleSMSJob($orderId)POSTQueue SMS resend job
getUsedPayways()GETDistinct payment methods used in orders

Available Filters:

FilterTypeDescription
searchLIKE (OR)Searches customer name/surname, send_to_email, send_to_phone, coupon code
dateFrom / dateToRangeCreated date range
paywayExactPayment method used
emailSentBooleanEmail delivery status
smsSentBooleanSMS delivery status
statusEnumGift card status (Pending/Completed/Canceled)

1.2 Status Lifecycle ​

Defined in ecommercen/gift_cards/libraries/GiftCardStatus.php (Spatie Enum):

StatusValueDescription
Pending1Payment initiated but not confirmed
Completed10Payment confirmed, coupon generated
Canceled11Order canceled (manual or auto-timeout)

Status filtering uses composite logic in AdvGiftCardOrdersModel::applyStatusFilter():

  • Pending: canceled_at IS NULL AND completed_at IS NULL
  • Completed: completed_at IS NOT NULL AND canceled_at IS NULL
  • Canceled: canceled_at IS NOT NULL

1.3 Accept Flow (Admin Action) ​

When an admin accepts a Pending or Canceled gift card order via the admin grid's Accept button (AdvGiftCardAdminListing::acceptGiftCard($orderId), :146-163): Canceled -> Completed is a legitimate admin transition, not an edge case — staff use it to confirm an out-of-band payment (over the phone, off a bank statement) on an order the AdvCancelPendingGiftCards cron already cancelled.

  1. The controller calls AdvGiftCardOrdersModel::acceptGiftCardManually($orderId) (AdvGiftCardOrdersModel.php:62-68) — the admin-only manual-accept override, deliberately wider than the plain acceptGiftCard() other callers use. It delegates to the shared acceptGiftCardFrom($orderId, [Pending, Canceled]) (:70-133), which runs the model's claim-then-issue-coupon algorithm — see CF-23 Gift Cards for the full steps (claim UPDATE, coupon creation, coupon linking, transaction completion). The only difference from the plain acceptGiftCard() path other callers use is the widened allow-list ([Pending, Canceled] instead of [Pending]).
  2. If the claim affects zero rows (:103-106) — the order was already Completed — trans_rollback() returns false from the model. AdvGiftCardAdminListing::acceptGiftCard() then answers HTTP 409 Conflict {"error": "This gift card order is already completed."} (AdvGiftCardAdminListing.php:149-152) and SKIPS acceptPostActions() entirely
  3. Only on success (the claim was won and the coupon issued), AdvGiftCardAdminListing::acceptGiftCard() calls acceptPostActions($orderId) (:161, empty by default, overrideable in client repos)

1.4 Cancel Flow (Admin Action) ​

When an admin cancels a gift card order (cancelGiftCard()):

  • If Pending: Simple status update to Canceled with canceled_at timestamp
  • If Completed: Transaction that deletes the linked coupon, nullifies coupon_id, sets status to Canceled

Calls cancelPostActions($orderId) hook after cancellation.

1.5 Notification Resend ​

Admins can manually trigger email or SMS resend for completed gift card orders:

  • Email resend: Schedules ResendGiftCardEmail job on QUEUE_GIFT_CARDS queue
  • SMS resend: Schedules ResendGiftCardSMS job on QUEUE_GIFT_CARDS queue

Both jobs validate that gift cards are enabled before executing. SMS additionally checks enabledSms setting.

1.6 Admin UI Error Handling ​

acceptOrder() and cancelOrder() in assets/admin/js/giftCards/components/GiftCardOrders.vue:402-420 previously had no .catch(), so a non-2xx response (e.g. the 409 in §1.3) failed silently and left the grid stale. Both now call the existing failedFetch() helper (:348) on rejection and re-run list() to refresh the grid. failedFetch() calls this.$toasted.clear() before showing the error toast, so a double-click that fires two overlapping requests shows one toast, not two stacked 409s.

The 409 is reachable only from a stale grid or a double-click: the Accept button renders only for gift_card_status === 11 (Canceled, :152-160) and gift_card_status === 1 (Pending, :162-174) — an already-Completed row has no Accept button in a freshly-loaded grid.


2. Gift Card Settings ​

Controller: ecommercen/gift_cards/controllers/AdvGiftCardSettings.php (81 lines) Auth: ADVISABLE, ADMIN, ORDERS Route: admin/giftCardsSettingsAdmin Menu: SETTINGS group

Vue-powered settings page. Reads/writes via AdvGiftCardSettingsReader and Registry.

Registry Keys (GIFT_CARDS group) ​

KeyTypeDescriptionRole Restriction
ENABLEDboolMaster toggle for gift card featureADVISABLE only
GIFT_CARDSarrayPredefined denomination amounts (sorted)--
FREE_AMOUNTboolAllow custom (free-form) amounts--
MIN_AMOUNTnumericMinimum gift card valueRequired
MAX_AMOUNTnumericMaximum gift card value (default 500)Required
SMSboolEnable SMS delivery--
ORDER_PREFIXstringPrefix for order serial numbers--

Payment Method Configuration ​

Gift card payment methods are configured separately from regular checkout in Adv_settings.php (payment settings):

  • Stored in Registry: METHODS.PAYWAY_GIFT_CARDS
  • Only shown when GIFT_CARDS.ENABLED is true
  • Supported payment gateways (from getGiftCardPayWays() in ecommercen/helpers/eshop_helper.php:352-373):
    • PayPal, PayPal Advanced, Eurobank, Alpha Bank, Piraeus, Ethniki, Viva Wallet, Ethniki EE, XPay (Nexi XPay Greece)
    • Iris is defined but commented out at :370
  • XPay is configured via the XPAY registry group; see IN-23 Nexi XPay Greece for XPay-specific configuration.
  • Viva Wallet has a dedicated GIFT_CARD_SOURCE_CODE registry key for gift card transactions

3. Promotional Gift Management ​

Controller: ecommercen/eshop/controllers/Adv_gifts_admin.php (442 lines) Auth: ADVISABLE, ADMIN, MARKETING Route: gifts_admin

This is a separate subsystem from gift card orders. Promotional gifts are free products automatically added to customer orders based on configurable rules.

CRUD Operations ​

ActionMethodDescription
Listindex($offset)Paginated list (50/page) with session-persisted search
Addadd()Create new gift rule with products, MUI, images
Editedit($id)Update existing gift rule
Deletedelete($id)Remove gift rule
TogglesetGiftActivity($id, $active)Enable/disable a gift
Searchsearch_append_item($mode)AJAX product search for gift/requirement assignment

Gift Data Fields ​

FieldTypeDescription
rule_idintGift rule type (from gift_rules_model)
amount_from / amount_todecimalCart value range trigger
internal_namestringAdmin-only label
date_start / date_enddateActive date range (required)
activeboolEnabled/disabled
gift_per_countintNumber of gifts per qualifying order (1-10)
gift_user_choice_countintHow many gifts the user can choose
is_promoboolPromotional flag
priorityintEvaluation priority
remainingint/nullStock limit (null = unlimited)

Requirements System ​

Gifts can have two types of requirements:

  1. Product requirements (dom_req_ID[]): Specific products that must be in cart
  2. Vendor requirements (req_vendor_ids[]): Products from specific vendors must be in cart

MUI Fields (per language) ​

FieldTypeDescription
descriptiontextCustomer-facing description
urlstringLink URL
imagefilePrimary image
promo_imagefilePromotional image
extra_vendor_imagefileAdditional vendor-context image
extra_product_imagefileAdditional product-context image

Images uploaded to files/gifts/ via advuploader.

Search Filters (Session-persisted) ​

FilterDescription
activeActive/inactive status
textFree text search
giftRulesFilter by rule type
maxStockMaximum remaining stock

Extension Hooks ​

The controller provides three empty protected methods for client repo overrides:

  • afterAdd($giftId) -- post-creation logic
  • afterEdit($giftId) -- post-update logic
  • afterDelete($giftId) -- post-deletion logic

4. Background Jobs ​

All gift card jobs are in the giftCards queue (QUEUE_GIFT_CARDS constant).

Scheduled Jobs (from application/config/jobs.php) ​

JobScheduleDescription
CancelPendingGiftCardsEvery 15 minAuto-cancel stale pending orders
SendGiftCardsToEmailsEvery 5 minSend emails for unsent completed orders
SendGiftCardsToPhonesEvery 5 minSend SMS for unsent completed orders

On-Demand Jobs (Admin-Triggered) ​

JobTriggerDescription
ResendGiftCardEmailAdmin buttonResend gift card email for specific order
ResendGiftCardSMSAdmin buttonResend gift card SMS for specific order

CancelPendingGiftCards Logic ​

Configured timeout: giftCardDateTimeIntervalToDrop (default: PT180M = 3 hours, in application/config/app.php:560; validated with fallback in AdvCancelPendingGiftCards.php:20, 65-92).

  1. Fetches pending orders older than the timeout (excluding Iris, PayPal Advanced, and XPay)
  2. Bulk cancels standard gateway orders
  3. Iris orders: Queries Iris API for actual payment status; accepts if Iris reports PAID, cancels on other statuses, skips unresolved orders for next run (ecommercen/gift_cards/jobs/AdvCancelPendingGiftCards.php:196-207)
  4. PayPal Advanced orders: Queries PayPal REST API for capture status; cancels if not COMPLETED, accepts if COMPLETED
  5. XPay orders: Queries Nexi XPay for order status via XPay::getOrderStatus(); accepts if PAID, cancels otherwise (AdvCancelPendingGiftCards.php:253-278). XPay orders are excluded from the bulk-cancel query at :43.

Email Delivery ​

Two email templates:

  • Recipient email (application/views/main/mail/gift_card.php): Sent to send_to_email with coupon code, amount, sender name, expiration date
  • Customer confirmation (application/views/main/mail/gift_card_inform_customer.php): Sent to purchasing customer confirming the gift card purchase was completed

Email subjects configured in db_default_values.php:

  • GIFT_CARD: "You have received a gift card!" (multi-language)
  • GIFT_CARD_INFORM_CUSTOMER: "Gift Card Purchase" (multi-language)

SMS Delivery ​

Handled by GiftCardSendSms library (ecommercen/gift_cards/jobs/GiftCardSendSms.php). Uses the customer's language preference for message localization. SMS provider configured via giftCardSMSProvider (default: yuboto).


5. Database Schema ​

Source: database/initial/initial.sql:589-619

gift_card_orders Table ​

sql
CREATE TABLE gift_card_orders (
    id               int(11)        NOT NULL AUTO_INCREMENT,
    customer_id      int(11)        NOT NULL,
    amount           decimal(11,2)  NOT NULL,
    currency_id      int(11)        NOT NULL,
    currency_rate    decimal(11,4)  NOT NULL,
    payway           varchar(255)   NOT NULL,
    gift_card_status tinyint(1)     NOT NULL,
    coupon_id        int(11)        DEFAULT NULL,
    send_to_email    varchar(255)   DEFAULT NULL,
    send_to_phone    varchar(255)   DEFAULT NULL,
    send_to_name     varchar(255)   DEFAULT NULL,
    send_to_surname  varchar(255)   DEFAULT NULL,
    message          text           DEFAULT NULL,
    email_sent       tinyint(1)     NOT NULL DEFAULT 0,
    sms_sent         tinyint(1)     NOT NULL DEFAULT 0,
    created_at       datetime       NOT NULL,
    completed_at     datetime       DEFAULT NULL,
    canceled_at      datetime       DEFAULT NULL,
    order_serial     varchar(255)   DEFAULT NULL,
    tran_ticket      varchar(32)    DEFAULT NULL,
    PRIMARY KEY (id)
);

Indexes: customer_id, coupon_id, gift_card_status, currency_id, created_at, payway, composite (gift_card_status, email_sent), composite (gift_card_status, sms_sent).

Foreign key relationships (logical, not enforced):

  • customer_id -> shop_customer.id
  • coupon_id -> coupons.coupon_id
  • currency_id -> currencies.id

6. Modern Domain Layer (REST API) ​

The gift card order entity has a full modern domain layer under the Order context.

Domain Components ​

ComponentPath
Entitysrc/Domains/Order/GiftCardOrder/Repository/Entity.php
Repositorysrc/Domains/Order/GiftCardOrder/Repository/Repository.php
RepositoryConfiguratorsrc/Domains/Order/GiftCardOrder/Repository/RepositoryConfigurator.php
WriteRepositorysrc/Domains/Order/GiftCardOrder/Repository/WriteRepository.php
Servicesrc/Domains/Order/GiftCardOrder/Service.php
WriteServicesrc/Domains/Order/GiftCardOrder/WriteService.php
WriteDatasrc/Domains/Order/GiftCardOrder/WriteData.php
Validatorsrc/Domains/Order/GiftCardOrder/Validator.php
ListRequestsrc/Domains/Order/GiftCardOrder/ListRequest.php

REST Controller ​

src/Rest/Order/Controllers/GiftCardOrder.php -- full CRUD with OpenAPI annotations.

MethodPathDescription
GET/rest/order/gift-card-orderCollection with filters/sorts
GET/rest/order/gift-card-order/itemSingle item by filter
GET/rest/order/gift-card-order/{id}Show by ID
POST/rest/order/gift-card-orderCreate
POST/rest/order/gift-card-order/{id}Update
DELETE/rest/order/gift-card-order/{id}Delete

Auth policy (rest_policies.php): Backend auth, roles ADMIN + ORDERS.

Available filters: id, customerId, currencyId, giftCardStatus, couponId, payway, emailSent, smsSent, sendToEmail (partial), sendToName (partial), orderSerial (partial).

Available sorts: id, customerId, amount, giftCardStatus, createdAt, completedAt.

DI Registration ​

  • Domain: src/Domains/Order/container.php -- registers Repository, RepositoryConfigurator, Service, WriteRepository, Validator, WriteService
  • REST: src/Rest/Order/container.php -- registers GiftCardOrder controller with service bindings

7. Payment Gateway Integration ​

Gift card orders integrate with payment gateways through the AdvGiftCardPage front controller (ecommercen/gift_cards/controllers/AdvGiftCardPage.php). Each gateway returns form data with a type (redirect, iframe, form) and URL.

Webhook Handling ​

Viva Wallet: ecommercen/webhooks/AdvViva.php is the sole Viva Wallet webhook surface -- a parallel REST webhook controller family (src/Rest/Webhooks/...) existed briefly but was deleted as permanently unreachable dead code in commit 033fccad5b (issue #721; it had no rest_policies.php entry, so every request inherited the backend-auth default and 401'd -- zero production hits in 7 days per #620). The webhook handler checks both shop_order (via $this->order_model->getRecord(...), ecommercen/webhooks/AdvViva.php:31-38) and gift_card_orders (via $this->gift_card_orders_model->getOrderByTransactionTicket(...), :40-44) tables for the transaction ticket. If the order is a gift card:

  • transactionPaymentCreated -> records installments, calls acceptGiftCard() (the Pending-only entry point — see CF-23 Gift Cards), branches on the return value, and logs when the accept is refused (order was not Pending)
  • transactionFailed -> calls cancelGiftCard()

Before either branch runs, handleWebhook() now verifies the notification against Viva's own transaction record via vivaWallet()->vivaWalletValidateOrderIsPaid() (ecommercen/webhooks/AdvViva.php:60-62) -- answering 502 if Viva is unreachable so its hourly retry is not lost (:64-74), and requiring gatewayConfirms() (:153-168) to confirm the order code, transaction validity, and amount (in integer minor units, :210-217, with a currency-converted transaction exempted from the amount check, :179-187) before the acceptGiftCard()/cancelGiftCard() branch executes. A failed gate is logged and answered with 200 without writing anything (:76-87); events outside transactionPaymentCreated/transactionFailed are acknowledged with 200 before Viva is even consulted (:8, :53-57).

Customer Order History Integration ​

Gift card orders appear alongside regular orders in the customer admin view (Adv_customers_admin.php::order_history()). The order model uses a UNION query (compileGiftOrdersSelect()) to merge gift card orders into the unified order timeline, with an is_gift_card flag to differentiate display.


8. Client Extension Points ​

Hook / OverrideLocationPurpose
acceptPostActions($orderId)AdvGiftCardAdminListingCustom logic after accepting a gift card
cancelPostActions($orderId)AdvGiftCardAdminListingCustom logic after canceling a gift card
indexExtras()AdvGiftCardAdminListingAdd extra data to listing page render
afterAdd($giftId)Adv_gifts_adminPost-creation hook for promotional gifts
afterEdit($giftId)Adv_gifts_adminPost-update hook for promotional gifts
afterDelete($giftId)Adv_gifts_adminPost-deletion hook for promotional gifts
AdvGiftCardSettingsReaderCan be overridden in client custom/Custom settings logic
GiftCardSendSmsCan be overriddenCustom SMS delivery
AdvGiftCardPage::acceptGiftCard()storefront controller seam, not on the admin code pathSee CF-23 Gift Cards Client Extension Points for this seam

Configuration ​

Registry keys for gift card feature and payment method configuration:

GroupKeyDefaultDescription
GIFT_CARDSENABLEDfalseMaster toggle
GIFT_CARDSGIFT_CARDS[]Preset denomination amounts
GIFT_CARDSFREE_AMOUNTfalseAllow custom amounts
GIFT_CARDSMIN_AMOUNT0Minimum value
GIFT_CARDSMAX_AMOUNT500Maximum value
GIFT_CARDSSMSfalseSMS delivery toggle
GIFT_CARDSORDER_PREFIX''Order serial prefix
METHODSPAYWAY_GIFT_CARDS[]Enabled payment methods
VIVAWALLETGIFT_CARD_SOURCE_CODE''Viva source code for gift cards

XPay (Nexi) configuration: See IN-23 Nexi XPay Greece for XPay-specific registry keys and settings.


Known Issues & Security Gaps ​

  1. Admin accept idempotency — RESOLVED (#583, commits f89660a84 + a9e5d9788). AdvGiftCardAdminListing::acceptGiftCard() (ecommercen/gift_cards/controllers/AdvGiftCardAdminListing.php:146-163) calls AdvGiftCardOrdersModel::acceptGiftCardManually() (ecommercen/gift_cards/models/AdvGiftCardOrdersModel.php:62-68) — NOT the plain acceptGiftCard() — a manual-accept entry point that widens the allow-list to [Pending, Canceled] but still refuses an already-Completed order. Both entry points delegate to the shared acceptGiftCardFrom() (:70-133), which claims the order with a single conditional UPDATE ... WHERE id = ? AND gift_card_status IN (...) before any coupon is issued or coupon_id is overwritten. Accepting an already-Completed order now returns false from the claim, and the controller answers HTTP 409 Conflict {"error": "This gift card order is already completed."} (AdvGiftCardAdminListing.php:149-152) instead of minting a second coupon. See §1.3 Accept Flow below and CF-23 Gift Cards for the model's shared claim-then-issue semantics, which this admin path now uses directly.

  2. Re-cancelling an already-Canceled order throws TypeError. AdvGiftCardOrdersModel::cancelGiftCard() (:135-145) routes any non-Pending order to transactionCancelGiftCard(int $orderId, int $couponId) (:162), but an already-Canceled order has coupon_id = NULL (nulled at :170; column is DEFAULT NULL). Re-cancelling one passes null to a non-nullable int parameter, throwing TypeError, surfaced as HTTP 500 via the controller's Throwable catch (AdvGiftCardAdminListing.php:127-131).

For XPay webhook and securityToken verification gaps, see CF-09 Payment Webhooks and IN-23 Nexi XPay Greece.


Tests ​

tests/Unit/Jobs/AdvCancelPendingGiftCardsTest.php — 13 methods ​

Coverage for cron reconciliation:

GroupDescription
Iris reconcile matrixData-driven tests for irisReconcileAction() state machine with provider at :490-491
dateTimeIntervalToDrop fallbacksValidation tests for the grace-period timeout with missing/malformed config values; fallback to DEFAULT_DATE_TIME_INTERVAL_TO_DROP = 'PT180M' at :20, 65-92

tests/Legacy/GiftCards/AdvGiftCardPageTest.php — 15 methods ​

GroupMethodsDescription
Piraeus integration12piraeusSuccessAction() / piraeusFailAction() state machine
XPay integration3xpayWebhookAction() state machine and decision matrix (covering all 6 status enum values × 3 XPay result categories)

tests/Legacy/GiftCards/AdvGiftCardOrdersModelTest.php — 10 #[Test] methods (559 lines, added by #583) ​

Covers acceptGiftCardFrom()'s shared claim: duplicate accept on a Completed order via both acceptGiftCard() and acceptGiftCardManually(), accept on a Canceled order via both entry points, the canceled_at clear on a Canceled -> Completed accept, the admin Completed-filter resolution (applyStatusFilter()), claim-before-coupon-issue ordering, and NotFoundException propagation from getGiftCard().

Test coverage gap — HALF closed by #583: The AdvGiftCardAdminListing accept/cancel HTTP endpoints themselves, AdvGiftCardOrdersModel::cancelGiftCard(), and the cron helper AdvCancelPendingGiftCards::cancelPendingXpayOrders() remain untested (verified by grep).