Appearance
Gift Cards
Flow ID: CF-23 | Module(s): gift_cards, coupons, Order domain | Complexity: Medium | Last Updated: 2026-05-21
Business Overview
Gift cards are purchased through a dedicated checkout flow (separate from cart), processed via payment gateways, and redeemed as single-use coupons. On payment success, a coupon code is generated and delivered via email/SMS.
Lifecycle: Purchase -> Pay -> Generate coupon -> Deliver code -> Redeem at checkout
What customers experience:
- Visit
/gift-cardpage and select a gift card amount (preset or free-form) - Enter recipient details (email and/or phone) and personal message
- Choose payment method and complete payment
- System generates a unique coupon code and sends it to the recipient
- Recipient enters the code at checkout to redeem the full amount
Key business behaviors:
- Coupon: type=GiftCard, max_usage=1, validity=5 years, min_cart=gift_amount
- Auto-cancel pending orders after
giftCardDateTimeIntervalToDrop(default 24h) - No partial redemption (full amount or nothing -- enforced by
total_cart_fromrule) - Separate checkout flow: not part of the regular cart/order pipeline
- Supports multiple payment gateways (PayPal, Eurobank, Piraeus, Viva Wallet, NBG, Iris, Alpha, PayPal Advanced, XPay)
- SMS delivery optional (gated by
GIFT_CARDS.SMSregistry setting) - Feature gated by
GIFT_CARDS.ENABLEDregistry setting
API Reference
REST Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /rest/order/gift-card-order | Backend | List gift card orders |
| POST | /rest/order/gift-card-order | Backend | Create gift card order |
| DELETE | /rest/order/gift-card-order/{id} | Backend | Delete |
Filters: customerId, giftCardStatus, couponId, payway, emailSent, smsSent
Legacy Storefront
| URL | Method | Description |
|---|---|---|
/gift-card | index() | Gift card purchase page (Vue-powered) |
/gift-card/checkout | checkout() | POST -- create pending order + get payment form data |
/gift-card/paypalSuccess/{orderId} | paypalSuccess() | PayPal return URL (success) |
/gift-card/paypalCancel/{orderId} | paypalCancel() | PayPal return URL (cancel) |
/gift-card/xPaySuccess/{orderSerial} | xPaySuccess() | XPay return URL — verifies via $xpay->getOrderStatus(), calls successView() on PAID |
/gift-card/xPayCancel/{orderSerial} | xPayCancel() | XPay cancel return URL — calls errorView() with checkout.xpay.fail.title language key |
/gift-card/xPayHook | xPayHook() | XPay server-to-server webhook — verifies securityToken with flow='gift_card', state machine |
XPay is the only gift-card gateway with a server-to-server webhook; all other gateways finalize on the return URL.
Code Flow
Step 1: Gift Card Page
File: ecommercen/gift_cards/controllers/AdvGiftCardPage.php::index()
- Feature gate:
_remap()returns 404 ifgiftCardsEnabled()is false - Settings:
GiftCardSettingsReaderloads config from Registry:ENABLED,FREE_AMOUNT,MIN_AMOUNT,MAX_AMOUNT,GIFT_CARDS(preset amounts),SMS,ORDER_PREFIX - Render: Vue-powered page (
giftCardPagelayout) withuseVue=true
Step 2: Checkout (Create Pending Order)
File: ecommercen/gift_cards/controllers/AdvGiftCardPage.php::checkout()
- Method check: POST only via
ensureMethodIs('post') - Parse input:
jsonDecodeInputStream()-- JSON body from Vue form - Validate: Form validation rules for amount, email, phone, payway, recipient details
- Customer:
getOrCreateCustomerId($postData)-- creates guest customer if needed - Create order:
createPendingGiftCard()-- inserts withgift_card_status=1(Pending) - Order serial:
orderSerial($orderId, $payway)-- generates unique reference with optional prefix - Payment form data:
getPayWayFormData()-- routes to gateway-specific handler - Response: JSON
{message: 'success', orderId, payWayFormData: {type, url, data}}
Step 3: Payment Gateway Integration
File: ecommercen/gift_cards/controllers/AdvGiftCardPage.php
Each gateway returns {type, url, data} where type is redirect or iframe:
| Gateway | Method | Type |
|---|---|---|
| PayPal Express | paypalFormData() | redirect |
| PayPal Advanced | paypalAdvancedFormData() | redirect |
| Eurobank | eurobankFormData() | redirect (form POST) |
| Piraeus | piraeusFormData() | redirect (ticket-based). Currently disabled — commented out in code, falls through to Alpha. |
| Alpha | alphaFormData() | redirect |
| NBG (Ethniki) | ethnikiFormData() | redirect |
| NBG EE | ethnikiEEFormData() | redirect |
| Viva Wallet | vivaWalletFormData() | redirect |
| Iris | irisFormData() | redirect |
| XPay | xpayFormData() | redirect (HPP) |
XPay (AdvGiftCardPage.php:1118-1166) uses Nexi XPay's Hosted Payment Page. It calls $xpay->createHostedPaymentOrder() with flow='gift_card', stashes the gateway-side orderId in gift_card_orders.tran_ticket, and returns {type: redirect, url: $hostedPage, data: []}. payWayInstallments['xpay'] is empty — XPay does not offer installments. Customer payload includes id, description (via checkout.xpay.order_description language key), email, and phone split via PhoneHelper.
XPay Return-URL Verification
File: ecommercen/gift_cards/controllers/AdvGiftCardPage.php::xPaySuccess() (lines 1168-1194)
- Lookup:
serialToId($orderSerial, 'xpay')→ fetch gift-card row - Verify upstream:
$xpay->getOrderStatus($orderSerial)— calls Nexi API; on failure falls through toxPayCancel() - Map status:
$xpay->mapOperationResultToStatus($operationResult) - On PAID:
successView($orderId)— triggersacceptGiftCard()+acceptPostActions() - Otherwise:
xPayCancel($orderSerial)(logged at info level)
xPayCancel() (lines 1196-1207) fetches the order and calls errorView() with checkout.xpay.fail.title. Same pattern as Iris and PayPal Advanced: verify via API on return, no trust placed on querystring data.
XPay Server-to-Server Webhook
File: ecommercen/gift_cards/controllers/AdvGiftCardPage.php::xPayHook() (lines 1218-1290)
XPay is the only gift-card gateway with a server-to-server webhook. The handler is separate from checkout/xPayHook because the lookup table differs (gift_card_orders vs orders).
- Parse
json_decode($input_stream)— HTTP 400 on invalid JSON $xpay->parsePaymentNotification()— HTTP 400 on exception$xpay->verifyNotificationSecurityToken($notification, $orderSerial, 'gift_card')— HTTP 401 on mismatch. Theflow='gift_card'namespace isolates gift-card tokens from regular-checkout tokens.serialToId() + gift_card_orders_model->get()— HTTP 404 if missing- REFUNDED notifications: ignored (logged, no state change)
- State machine via
xpayWebhookAction(GiftCardStatus, string): string:
| Current status | XPay status | Action |
|---|---|---|
| Pending | PAID | accept (issue coupon, mark Completed) |
| Pending | CANCELED | cancel |
| Completed | CANCELED | cancel (revoke coupon — reversal) |
| Completed | PAID | noop (idempotency guard — prevents duplicate coupons) |
| Canceled | any | noop (terminal) |
| any | PENDING/UNKNOWN/empty | noop |
xpayWebhookAction() is a public static pure helper unit-tested in tests/Legacy/GiftCards/AdvGiftCardPageTest.php with 10 data-provider rows + 2 exhaustive invariant guards.
Step 4: Payment Acceptance (Coupon Generation)
File: ecommercen/gift_cards/models/AdvGiftCardOrdersModel.php::acceptGiftCard()
- Fetch order:
getGiftCard($orderId)-- throws if not found - Create coupon: Inserts coupon with
name=GIFTCARD_{orderId},discount_price=amount,max_count_usage=1,coupon_type=GiftCard - Coupon rules:
total_cart_from=amount(minimum cart for redemption),date_start=now,date_end=now+5years - Generate code:
coupons_model->generateCoupons($couponId, 1)-- creates unique redeemable code - Update order: Set
gift_card_status=10(Completed),completed_at=now,coupon_id,email_sent=false,sms_sent=false - Transaction: All wrapped in
trans_begin()/trans_complete()-- rolls back on failure
Step 5: Email Delivery Job
File: ecommercen/gift_cards/jobs/AdvSendGiftCardsToEmails.php
- Feature gate: Check
enabledGiftCardssetting - Query: Completed orders where
email_sent=falseandsend_to_emailis set - Send to recipient:
adv_mailer->sendGiftCardToEmail($order)-- delivers coupon code - Inform purchaser:
adv_mailer->sendGiftCardToInformCustomer($order)-- confirms delivery - Mark sent:
markGiftCardMailSent($orderId)-- setsemail_sent=trueand marks coupon as sent
Step 6: SMS Delivery Job
File: ecommercen/gift_cards/jobs/AdvSendGiftCardsToPhones.php
- Feature gate: Check
enabledGiftCardsANDenabledSmssettings - Query: Completed orders where
sms_sent=falseandsend_to_phoneis set - Send:
GiftCardSendSms->sendSms($order)per order - Mark sent: Updates
sms_sent=true
Step 7: Auto-Cancel Pending Orders
File: ecommercen/gift_cards/jobs/AdvCancelPendingGiftCards.php
- Cutoff date:
now - giftCardDateTimeIntervalToDrop(config, defaultP1D= 24 hours) - Regular orders: Batch-cancel all pending orders older than cutoff (except
payway not in ('iris','paypaladvanced','xpay')) - Iris orders: Check each order status via Iris API -- cancel if
CANCELED, accept if paid - PayPal Advanced: Check each order via PayPal REST API -- cancel if not
COMPLETED, accept if paid - XPay orders:
cancelPendingXpayOrders()— for each pending XPay row, calls$xpay->getOrderStatus($order_serial)and$xpay->mapOperationResultToStatus(). Accepts if PAID, cancels otherwise (ecommercen/gift_cards/jobs/AdvCancelPendingGiftCards.php:91-114).
Step 8: Order Cancellation
File: ecommercen/gift_cards/models/AdvGiftCardOrdersModel.php::cancelGiftCard()
- Pending orders: Simply update
gift_card_status=11(Canceled),canceled_at=now - Completed orders: Delete the generated coupon via
coupons_model->deleteRecord(), then cancel the order (wrapped in transaction)
Domain Layer
| Component | Path |
|---|---|
| Service | src/Domains/Order/GiftCardOrder/Service.php |
| WriteService | src/Domains/Order/GiftCardOrder/WriteService.php |
| Entity | src/Domains/Order/GiftCardOrder/Repository/Entity.php |
| Legacy Controller | ecommercen/gift_cards/controllers/AdvGiftCardPage.php |
| Admin Controller | ecommercen/gift_cards/controllers/AdvGiftCardAdminListing.php |
| Settings Controller | ecommercen/gift_cards/controllers/AdvGiftCardSettings.php |
| Legacy Model | ecommercen/gift_cards/models/AdvGiftCardOrdersModel.php |
| Status Enum | ecommercen/gift_cards/libraries/GiftCardStatus.php (Spatie Enum) |
| Settings Reader | ecommercen/gift_cards/libraries/AdvGiftCardSettingsReader.php |
| Email Job | ecommercen/gift_cards/jobs/AdvSendGiftCardsToEmails.php |
| SMS Job | ecommercen/gift_cards/jobs/AdvSendGiftCardsToPhones.php |
| Cancel Job | ecommercen/gift_cards/jobs/AdvCancelPendingGiftCards.php |
| Resend Email Job | ecommercen/gift_cards/jobs/AdvResendGiftCardEmail.php |
| Resend SMS Job | ecommercen/gift_cards/jobs/AdvResendGiftCardSMS.php |
| SMS Sender | ecommercen/gift_cards/jobs/GiftCardSendSms.php |
Status values (GiftCardStatus Spatie Enum): Pending=1, Completed=10, Canceled=11.
Data Model
gift_card_orders
| Column | Type | Description |
|---|---|---|
id | int (PK, AI) | Gift card order ID |
customer_id | int (FK) | Purchasing customer |
amount | decimal(11,2) | Gift card face value |
currency_id | int (FK) | Currency reference |
currency_rate | decimal(11,4) | Exchange rate at time of purchase |
payway | varchar(255) | Payment method identifier |
gift_card_status | tinyint(1) | Status: 1=Pending, 10=Completed, 11=Canceled |
coupon_id | int (FK, nullable) | Generated coupon reference (set on completion) |
send_to_email | varchar(255, nullable) | Recipient email |
send_to_phone | varchar(255, nullable) | Recipient phone (for SMS) |
send_to_name | varchar(255, nullable) | Recipient first name |
send_to_surname | varchar(255, nullable) | Recipient last name |
message | text (nullable) | Personal message from purchaser |
email_sent | tinyint(1) | Whether email was delivered |
sms_sent | tinyint(1) | Whether SMS was delivered |
created_at | datetime | Order creation timestamp |
completed_at | datetime (nullable) | When payment was confirmed |
canceled_at | datetime (nullable) | When order was canceled |
order_serial | varchar(255, nullable) | Unique payment reference (prefix + ID + payway) |
tran_ticket | varchar(32, nullable) | Payment gateway transaction ticket |
Indexes: customer_id, coupon_id, gift_card_status, currency_id, created_at, payway, status+email_sent, status+sms_sent
Registry Settings
| Group | Key | Description |
|---|---|---|
GIFT_CARDS | ENABLED | Feature toggle (boolean) |
GIFT_CARDS | MIN_AMOUNT | Minimum gift card amount |
GIFT_CARDS | MAX_AMOUNT | Maximum gift card amount |
GIFT_CARDS | FREE_AMOUNT | Whether custom amounts are allowed |
GIFT_CARDS | GIFT_CARDS | Array of preset gift card amounts |
GIFT_CARDS | SMS | Whether SMS delivery is enabled |
GIFT_CARDS | ORDER_PREFIX | Prefix for order serial numbers |
Client Extension Points
acceptPostActions()/cancelPostActions()hooks: Custom logic on accept/cancelindexExtras()hook: Customize gift card purchase page- Payment gateways: Override
getPayWayFormData()to add custom gateways - Job queue:
AdvSendGiftCardsToEmails,AdvSendGiftCardsToPhones,AdvCancelPendingGiftCards - Resend jobs:
AdvResendGiftCardEmail,AdvResendGiftCardSMSfor manual retry - Registry settings: All gift card behavior configurable via
GIFT_CARDSregistry group - Views:
{client_views}/gift_cards/-- purchase page, success/error views, email templates
Known Issues & Security Gaps
No known issues at the time of writing.
Related Flows
- CF-08 Payment Processing -- gift card payment flow (separate from cart checkout)
- CF-09 Payment Webhooks -- Viva/Iris handle gift card payment callbacks. XPay is the only gift-card gateway with a dedicated
gift-card/xPayHookendpoint; all other callbacks use return-URL redirects. - CF-13 Coupons -- gift cards generate single-use coupons on completion
- AD-23 Gift Cards Admin -- admin management of gift card orders
- SY-24 Email Dispatch --
adv_mailerdelivery infrastructure for gift card emails