Appearance
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):
| Method | Action | Description |
|---|---|---|
list() | GET with query params | Paginated, filtered list (50 per page) |
get($orderId) | GET | Single gift card order detail |
cancelGiftCard($orderId) | POST | Cancel a gift card order |
acceptGiftCard($orderId) | POST | Accept/complete a gift card order; refuses an already-Completed order with 409 Conflict (see §1.3) |
scheduleEmailJob($orderId) | POST | Queue email resend job |
scheduleSMSJob($orderId) | POST | Queue SMS resend job |
getUsedPayways() | GET | Distinct payment methods used in orders |
Available Filters:
| Filter | Type | Description |
|---|---|---|
search | LIKE (OR) | Searches customer name/surname, send_to_email, send_to_phone, coupon code |
dateFrom / dateTo | Range | Created date range |
payway | Exact | Payment method used |
emailSent | Boolean | Email delivery status |
smsSent | Boolean | SMS delivery status |
status | Enum | Gift card status (Pending/Completed/Canceled) |
1.2 Status Lifecycle
Defined in ecommercen/gift_cards/libraries/GiftCardStatus.php (Spatie Enum):
| Status | Value | Description |
|---|---|---|
| Pending | 1 | Payment initiated but not confirmed |
| Completed | 10 | Payment confirmed, coupon generated |
| Canceled | 11 | Order 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.
- The controller calls
AdvGiftCardOrdersModel::acceptGiftCardManually($orderId)(AdvGiftCardOrdersModel.php:62-68) — the admin-only manual-accept override, deliberately wider than the plainacceptGiftCard()other callers use. It delegates to the sharedacceptGiftCardFrom($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 plainacceptGiftCard()path other callers use is the widened allow-list ([Pending, Canceled]instead of[Pending]). - If the claim affects zero rows (
:103-106) — the order was alreadyCompleted—trans_rollback()returnsfalsefrom the model.AdvGiftCardAdminListing::acceptGiftCard()then answers HTTP 409 Conflict{"error": "This gift card order is already completed."}(AdvGiftCardAdminListing.php:149-152) and SKIPSacceptPostActions()entirely - Only on success (the claim was won and the coupon issued),
AdvGiftCardAdminListing::acceptGiftCard()callsacceptPostActions($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_attimestamp - 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
ResendGiftCardEmailjob onQUEUE_GIFT_CARDSqueue - SMS resend: Schedules
ResendGiftCardSMSjob onQUEUE_GIFT_CARDSqueue
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)
| Key | Type | Description | Role Restriction |
|---|---|---|---|
ENABLED | bool | Master toggle for gift card feature | ADVISABLE only |
GIFT_CARDS | array | Predefined denomination amounts (sorted) | -- |
FREE_AMOUNT | bool | Allow custom (free-form) amounts | -- |
MIN_AMOUNT | numeric | Minimum gift card value | Required |
MAX_AMOUNT | numeric | Maximum gift card value (default 500) | Required |
SMS | bool | Enable SMS delivery | -- |
ORDER_PREFIX | string | Prefix 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.ENABLEDis true - Supported payment gateways (from
getGiftCardPayWays()inecommercen/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
XPAYregistry group; see IN-23 Nexi XPay Greece for XPay-specific configuration. - Viva Wallet has a dedicated
GIFT_CARD_SOURCE_CODEregistry 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
| Action | Method | Description |
|---|---|---|
| List | index($offset) | Paginated list (50/page) with session-persisted search |
| Add | add() | Create new gift rule with products, MUI, images |
| Edit | edit($id) | Update existing gift rule |
| Delete | delete($id) | Remove gift rule |
| Toggle | setGiftActivity($id, $active) | Enable/disable a gift |
| Search | search_append_item($mode) | AJAX product search for gift/requirement assignment |
Gift Data Fields
| Field | Type | Description |
|---|---|---|
rule_id | int | Gift rule type (from gift_rules_model) |
amount_from / amount_to | decimal | Cart value range trigger |
internal_name | string | Admin-only label |
date_start / date_end | date | Active date range (required) |
active | bool | Enabled/disabled |
gift_per_count | int | Number of gifts per qualifying order (1-10) |
gift_user_choice_count | int | How many gifts the user can choose |
is_promo | bool | Promotional flag |
priority | int | Evaluation priority |
remaining | int/null | Stock limit (null = unlimited) |
Requirements System
Gifts can have two types of requirements:
- Product requirements (
dom_req_ID[]): Specific products that must be in cart - Vendor requirements (
req_vendor_ids[]): Products from specific vendors must be in cart
MUI Fields (per language)
| Field | Type | Description |
|---|---|---|
description | text | Customer-facing description |
url | string | Link URL |
image | file | Primary image |
promo_image | file | Promotional image |
extra_vendor_image | file | Additional vendor-context image |
extra_product_image | file | Additional product-context image |
Images uploaded to files/gifts/ via advuploader.
Search Filters (Session-persisted)
| Filter | Description |
|---|---|
active | Active/inactive status |
text | Free text search |
giftRules | Filter by rule type |
maxStock | Maximum remaining stock |
Extension Hooks
The controller provides three empty protected methods for client repo overrides:
afterAdd($giftId)-- post-creation logicafterEdit($giftId)-- post-update logicafterDelete($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)
| Job | Schedule | Description |
|---|---|---|
CancelPendingGiftCards | Every 15 min | Auto-cancel stale pending orders |
SendGiftCardsToEmails | Every 5 min | Send emails for unsent completed orders |
SendGiftCardsToPhones | Every 5 min | Send SMS for unsent completed orders |
On-Demand Jobs (Admin-Triggered)
| Job | Trigger | Description |
|---|---|---|
ResendGiftCardEmail | Admin button | Resend gift card email for specific order |
ResendGiftCardSMS | Admin button | Resend 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).
- Fetches pending orders older than the timeout (excluding Iris, PayPal Advanced, and XPay)
- Bulk cancels standard gateway orders
- 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) - PayPal Advanced orders: Queries PayPal REST API for capture status; cancels if not COMPLETED, accepts if COMPLETED
- XPay orders: Queries Nexi XPay for order status via
XPay::getOrderStatus(); accepts ifPAID, 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 tosend_to_emailwith 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.idcoupon_id->coupons.coupon_idcurrency_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
| Component | Path |
|---|---|
| Entity | src/Domains/Order/GiftCardOrder/Repository/Entity.php |
| Repository | src/Domains/Order/GiftCardOrder/Repository/Repository.php |
| RepositoryConfigurator | src/Domains/Order/GiftCardOrder/Repository/RepositoryConfigurator.php |
| WriteRepository | src/Domains/Order/GiftCardOrder/Repository/WriteRepository.php |
| Service | src/Domains/Order/GiftCardOrder/Service.php |
| WriteService | src/Domains/Order/GiftCardOrder/WriteService.php |
| WriteData | src/Domains/Order/GiftCardOrder/WriteData.php |
| Validator | src/Domains/Order/GiftCardOrder/Validator.php |
| ListRequest | src/Domains/Order/GiftCardOrder/ListRequest.php |
REST Controller
src/Rest/Order/Controllers/GiftCardOrder.php -- full CRUD with OpenAPI annotations.
| Method | Path | Description |
|---|---|---|
| GET | /rest/order/gift-card-order | Collection with filters/sorts |
| GET | /rest/order/gift-card-order/item | Single item by filter |
| GET | /rest/order/gift-card-order/{id} | Show by ID |
| POST | /rest/order/gift-card-order | Create |
| 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, callsacceptGiftCard()(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-> callscancelGiftCard()
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 / Override | Location | Purpose |
|---|---|---|
acceptPostActions($orderId) | AdvGiftCardAdminListing | Custom logic after accepting a gift card |
cancelPostActions($orderId) | AdvGiftCardAdminListing | Custom logic after canceling a gift card |
indexExtras() | AdvGiftCardAdminListing | Add extra data to listing page render |
afterAdd($giftId) | Adv_gifts_admin | Post-creation hook for promotional gifts |
afterEdit($giftId) | Adv_gifts_admin | Post-update hook for promotional gifts |
afterDelete($giftId) | Adv_gifts_admin | Post-deletion hook for promotional gifts |
AdvGiftCardSettingsReader | Can be overridden in client custom/ | Custom settings logic |
GiftCardSendSms | Can be overridden | Custom SMS delivery |
AdvGiftCardPage::acceptGiftCard() | storefront controller seam, not on the admin code path | See CF-23 Gift Cards Client Extension Points for this seam |
Configuration
Registry keys for gift card feature and payment method configuration:
| Group | Key | Default | Description |
|---|---|---|---|
| GIFT_CARDS | ENABLED | false | Master toggle |
| GIFT_CARDS | GIFT_CARDS | [] | Preset denomination amounts |
| GIFT_CARDS | FREE_AMOUNT | false | Allow custom amounts |
| GIFT_CARDS | MIN_AMOUNT | 0 | Minimum value |
| GIFT_CARDS | MAX_AMOUNT | 500 | Maximum value |
| GIFT_CARDS | SMS | false | SMS delivery toggle |
| GIFT_CARDS | ORDER_PREFIX | '' | Order serial prefix |
| METHODS | PAYWAY_GIFT_CARDS | [] | Enabled payment methods |
| VIVAWALLET | GIFT_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
Admin accept idempotency — RESOLVED (#583, commits f89660a84 + a9e5d9788).
AdvGiftCardAdminListing::acceptGiftCard()(ecommercen/gift_cards/controllers/AdvGiftCardAdminListing.php:146-163) callsAdvGiftCardOrdersModel::acceptGiftCardManually()(ecommercen/gift_cards/models/AdvGiftCardOrdersModel.php:62-68) — NOT the plainacceptGiftCard()— a manual-accept entry point that widens the allow-list to[Pending, Canceled]but still refuses an already-Completedorder. Both entry points delegate to the sharedacceptGiftCardFrom()(:70-133), which claims the order with a single conditionalUPDATE ... WHERE id = ? AND gift_card_status IN (...)before any coupon is issued orcoupon_idis overwritten. Accepting an already-Completedorder now returnsfalsefrom 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.Re-cancelling an already-Canceled order throws TypeError.
AdvGiftCardOrdersModel::cancelGiftCard()(:135-145) routes any non-Pendingorder totransactionCancelGiftCard(int $orderId, int $couponId)(:162), but an already-Canceledorder hascoupon_id = NULL(nulled at:170; column isDEFAULT NULL). Re-cancelling one passesnullto a non-nullableintparameter, throwingTypeError, surfaced as HTTP 500 via the controller'sThrowablecatch (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:
| Group | Description |
|---|---|
| Iris reconcile matrix | Data-driven tests for irisReconcileAction() state machine with provider at :490-491 |
dateTimeIntervalToDrop fallbacks | Validation 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
| Group | Methods | Description |
|---|---|---|
| Piraeus integration | 12 | piraeusSuccessAction() / piraeusFailAction() state machine |
| XPay integration | 3 | xpayWebhookAction() 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).
9. Related Flows
- CF-23 Gift Cards -- customer-facing purchase flow
- CF-13 Coupons -- gift cards create single-use coupons
- CF-09 Payment Webhooks -- Viva Wallet handles gift card payments
- AD-04 Customer Management -- gift card orders in customer order history
- AD-08 Coupon Management -- gift cards generate coupons with
coupon_type = GiftCard(distinct from standard coupons) - AD-09 Gift Rules -- promotional gift rules (free products in cart; distinct from monetary gift cards)
- AD-13 Settings -- payment method configuration for gift cards