Skip to content

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-card page 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_from rule)
  • 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.SMS registry setting)
  • Feature gated by GIFT_CARDS.ENABLED registry setting

API Reference

REST Endpoints

MethodPathAuthDescription
GET/rest/order/gift-card-orderBackendList gift card orders
POST/rest/order/gift-card-orderBackendCreate gift card order
DELETE/rest/order/gift-card-order/{id}BackendDelete

Filters: customerId, giftCardStatus, couponId, payway, emailSent, smsSent

Legacy Storefront

URLMethodDescription
/gift-cardindex()Gift card purchase page (Vue-powered)
/gift-card/checkoutcheckout()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/xPayHookxPayHook()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()

  1. Feature gate: _remap() returns 404 if giftCardsEnabled() is false
  2. Settings: GiftCardSettingsReader loads config from Registry: ENABLED, FREE_AMOUNT, MIN_AMOUNT, MAX_AMOUNT, GIFT_CARDS (preset amounts), SMS, ORDER_PREFIX
  3. Render: Vue-powered page (giftCardPage layout) with useVue=true

Step 2: Checkout (Create Pending Order)

File: ecommercen/gift_cards/controllers/AdvGiftCardPage.php::checkout()

  1. Method check: POST only via ensureMethodIs('post')
  2. Parse input: jsonDecodeInputStream() -- JSON body from Vue form
  3. Validate: Form validation rules for amount, email, phone, payway, recipient details
  4. Customer: getOrCreateCustomerId($postData) -- creates guest customer if needed
  5. Create order: createPendingGiftCard() -- inserts with gift_card_status=1 (Pending)
  6. Order serial: orderSerial($orderId, $payway) -- generates unique reference with optional prefix
  7. Payment form data: getPayWayFormData() -- routes to gateway-specific handler
  8. 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:

GatewayMethodType
PayPal ExpresspaypalFormData()redirect
PayPal AdvancedpaypalAdvancedFormData()redirect
EurobankeurobankFormData()redirect (form POST)
PiraeuspiraeusFormData()redirect (ticket-based). Currently disabled — commented out in code, falls through to Alpha.
AlphaalphaFormData()redirect
NBG (Ethniki)ethnikiFormData()redirect
NBG EEethnikiEEFormData()redirect
Viva WalletvivaWalletFormData()redirect
IrisirisFormData()redirect
XPayxpayFormData()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)

  1. Lookup: serialToId($orderSerial, 'xpay') → fetch gift-card row
  2. Verify upstream: $xpay->getOrderStatus($orderSerial) — calls Nexi API; on failure falls through to xPayCancel()
  3. Map status: $xpay->mapOperationResultToStatus($operationResult)
  4. On PAID: successView($orderId) — triggers acceptGiftCard() + acceptPostActions()
  5. 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).

  1. Parse json_decode($input_stream) — HTTP 400 on invalid JSON
  2. $xpay->parsePaymentNotification() — HTTP 400 on exception
  3. $xpay->verifyNotificationSecurityToken($notification, $orderSerial, 'gift_card') — HTTP 401 on mismatch. The flow='gift_card' namespace isolates gift-card tokens from regular-checkout tokens.
  4. serialToId() + gift_card_orders_model->get() — HTTP 404 if missing
  5. REFUNDED notifications: ignored (logged, no state change)
  6. State machine via xpayWebhookAction(GiftCardStatus, string): string:
Current statusXPay statusAction
PendingPAIDaccept (issue coupon, mark Completed)
PendingCANCELEDcancel
CompletedCANCELEDcancel (revoke coupon — reversal)
CompletedPAIDnoop (idempotency guard — prevents duplicate coupons)
Canceledanynoop (terminal)
anyPENDING/UNKNOWN/emptynoop

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()

  1. Fetch order: getGiftCard($orderId) -- throws if not found
  2. Create coupon: Inserts coupon with name=GIFTCARD_{orderId}, discount_price=amount, max_count_usage=1, coupon_type=GiftCard
  3. Coupon rules: total_cart_from=amount (minimum cart for redemption), date_start=now, date_end=now+5years
  4. Generate code: coupons_model->generateCoupons($couponId, 1) -- creates unique redeemable code
  5. Update order: Set gift_card_status=10 (Completed), completed_at=now, coupon_id, email_sent=false, sms_sent=false
  6. Transaction: All wrapped in trans_begin()/trans_complete() -- rolls back on failure

Step 5: Email Delivery Job

File: ecommercen/gift_cards/jobs/AdvSendGiftCardsToEmails.php

  1. Feature gate: Check enabledGiftCards setting
  2. Query: Completed orders where email_sent=false and send_to_email is set
  3. Send to recipient: adv_mailer->sendGiftCardToEmail($order) -- delivers coupon code
  4. Inform purchaser: adv_mailer->sendGiftCardToInformCustomer($order) -- confirms delivery
  5. Mark sent: markGiftCardMailSent($orderId) -- sets email_sent=true and marks coupon as sent

Step 6: SMS Delivery Job

File: ecommercen/gift_cards/jobs/AdvSendGiftCardsToPhones.php

  1. Feature gate: Check enabledGiftCards AND enabledSms settings
  2. Query: Completed orders where sms_sent=false and send_to_phone is set
  3. Send: GiftCardSendSms->sendSms($order) per order
  4. Mark sent: Updates sms_sent=true

Step 7: Auto-Cancel Pending Orders

File: ecommercen/gift_cards/jobs/AdvCancelPendingGiftCards.php

  1. Cutoff date: now - giftCardDateTimeIntervalToDrop (config, default P1D = 24 hours)
  2. Regular orders: Batch-cancel all pending orders older than cutoff (except payway not in ('iris','paypaladvanced','xpay'))
  3. Iris orders: Check each order status via Iris API -- cancel if CANCELED, accept if paid
  4. PayPal Advanced: Check each order via PayPal REST API -- cancel if not COMPLETED, accept if paid
  5. 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

ComponentPath
Servicesrc/Domains/Order/GiftCardOrder/Service.php
WriteServicesrc/Domains/Order/GiftCardOrder/WriteService.php
Entitysrc/Domains/Order/GiftCardOrder/Repository/Entity.php
Legacy Controllerecommercen/gift_cards/controllers/AdvGiftCardPage.php
Admin Controllerecommercen/gift_cards/controllers/AdvGiftCardAdminListing.php
Settings Controllerecommercen/gift_cards/controllers/AdvGiftCardSettings.php
Legacy Modelecommercen/gift_cards/models/AdvGiftCardOrdersModel.php
Status Enumecommercen/gift_cards/libraries/GiftCardStatus.php (Spatie Enum)
Settings Readerecommercen/gift_cards/libraries/AdvGiftCardSettingsReader.php
Email Jobecommercen/gift_cards/jobs/AdvSendGiftCardsToEmails.php
SMS Jobecommercen/gift_cards/jobs/AdvSendGiftCardsToPhones.php
Cancel Jobecommercen/gift_cards/jobs/AdvCancelPendingGiftCards.php
Resend Email Jobecommercen/gift_cards/jobs/AdvResendGiftCardEmail.php
Resend SMS Jobecommercen/gift_cards/jobs/AdvResendGiftCardSMS.php
SMS Senderecommercen/gift_cards/jobs/GiftCardSendSms.php

Status values (GiftCardStatus Spatie Enum): Pending=1, Completed=10, Canceled=11.


Data Model

gift_card_orders

ColumnTypeDescription
idint (PK, AI)Gift card order ID
customer_idint (FK)Purchasing customer
amountdecimal(11,2)Gift card face value
currency_idint (FK)Currency reference
currency_ratedecimal(11,4)Exchange rate at time of purchase
paywayvarchar(255)Payment method identifier
gift_card_statustinyint(1)Status: 1=Pending, 10=Completed, 11=Canceled
coupon_idint (FK, nullable)Generated coupon reference (set on completion)
send_to_emailvarchar(255, nullable)Recipient email
send_to_phonevarchar(255, nullable)Recipient phone (for SMS)
send_to_namevarchar(255, nullable)Recipient first name
send_to_surnamevarchar(255, nullable)Recipient last name
messagetext (nullable)Personal message from purchaser
email_senttinyint(1)Whether email was delivered
sms_senttinyint(1)Whether SMS was delivered
created_atdatetimeOrder creation timestamp
completed_atdatetime (nullable)When payment was confirmed
canceled_atdatetime (nullable)When order was canceled
order_serialvarchar(255, nullable)Unique payment reference (prefix + ID + payway)
tran_ticketvarchar(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

GroupKeyDescription
GIFT_CARDSENABLEDFeature toggle (boolean)
GIFT_CARDSMIN_AMOUNTMinimum gift card amount
GIFT_CARDSMAX_AMOUNTMaximum gift card amount
GIFT_CARDSFREE_AMOUNTWhether custom amounts are allowed
GIFT_CARDSGIFT_CARDSArray of preset gift card amounts
GIFT_CARDSSMSWhether SMS delivery is enabled
GIFT_CARDSORDER_PREFIXPrefix for order serial numbers

Client Extension Points

  • acceptPostActions() / cancelPostActions() hooks: Custom logic on accept/cancel
  • indexExtras() hook: Customize gift card purchase page
  • Payment gateways: Override getPayWayFormData() to add custom gateways
  • Job queue: AdvSendGiftCardsToEmails, AdvSendGiftCardsToPhones, AdvCancelPendingGiftCards
  • Resend jobs: AdvResendGiftCardEmail, AdvResendGiftCardSMS for manual retry
  • Registry settings: All gift card behavior configurable via GIFT_CARDS registry 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.