> ## Documentation Index
> Fetch the complete documentation index at: https://cona.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Gift card discounts

# Gift Card Discounts

This page explains how CONA retrieves and accounts for discounts applied to
Shopify gift cards — both at **purchase** time and at **redemption** time.

The redemption flow is non-obvious because Shopify does not tell us which
gift card a customer used on their current order — we have to walk back through
transaction receipts to the original purchase order to recover the discount.
This doc exists so future devs don't have to re-derive it from scratch.

## Why This Is Tricky

At redemption, the Shopify order only tells us:

* "A gift card paid `X` of this order"
* The transaction has `gateway: "gift_card"` and a `receiptJson` blob

It does **not** directly tell us:

* Whether that gift card was **bought** (liability on our books) or **manually
  issued** by the merchant (no liability)
* Whether the gift card was **discounted at purchase** (e.g. €50 card bought
  for €40) — which matters because the liability we booked is only €40, not €50

Without this, two kinds of accounting bugs appear:

1. Manually-issued cards get treated as liability reductions, reducing a
   liability that was never booked.
2. Discounted cards redeem at full face value, blowing a hole in the
   liability account (we booked €40, customer redeems €50).

The flow below solves both.

## The Three Accounting Scenarios

| Scenario                                   | Purchase booking                                                      | Redemption booking                                                                                                                                                                                 |
| ------------------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Manually-issued** card                   | No purchase — nothing booked                                          | Treat redemption as a plain `discount` line item on the sales invoice. No payment document.                                                                                                        |
| **Purchased** card, no discount            | Full face value → liability                                           | `gift_card_payment` document reduces liability by redeemed amount                                                                                                                                  |
| **Purchased** card, discounted at purchase | Face value + `gift_card_discount` line → net paid amount to liability | Redemption applies both a proportional `gift_card_discount` line on the sales invoice **and** a reduced `gift_card_payment` for the net amount. Discount is split across tax rates proportionally. |

## High-Level Flow on Redemption

```mermaid theme={null}
flowchart TD
    A[Shopify order fetched] --> B{Any transaction with<br/>gateway = gift_card?}
    B -->|No| Z[Regular order<br/>transformation]
    B -->|Yes| C[Extract GiftCard GIDs<br/>from receiptJson.gift_card_id]
    C --> D[getGiftCardOriginsActivity<br/>batch GraphQL lookup]
    D --> E{For each GiftCard node}
    E --> F[GiftCard.order == null?]
    F -->|Yes| G[Manual card<br/>→ manualGids Set]
    F -->|No| H[Purchased card — inspect<br/>original order's gift card lines]
    H --> I{Any discountAllocations<br/>on isGiftCard lines?}
    I -->|No| J[No discount entry<br/>liability = face value]
    I -->|Yes| K[Compute discountRatio<br/>= giftCardDiscount / giftCardFaceValue]
    K --> L[discounts Map entry:<br/>initialValue, discountAmount,<br/>discountRatio]
    G --> M[Serialize Set → string array<br/>Map → Record for Temporal]
    J --> M
    L --> M
    M --> N[Ship via workflow metadata]
    N --> O[enrichResourcesWithGiftCardData<br/>rehydrates Set + Map]
    O --> P[shopifyOrdersAdapter<br/>transformToDocuments]
    P --> Q[Sales invoice +<br/>gift_card_payment docs<br/>with correct amounts]
```

## Key Insight: The `receiptJson` → GiftCard GID Bridge

Shopify's gift card transaction carries a `receiptJson` field that — for
gateway `"gift_card"` — includes the specific `gift_card_id` used. That ID is
what we lift via `extractGiftCardGidFromReceipt` and use as the deterministic
link back to the **original** `GiftCard` node. From the GiftCard node we can
navigate to `GiftCard.order`, which is the purchase order (or `null` for
manual cards).

This is the **only** reliable way; do not try to match on code, balance, or
customer — those are all ambiguous.

## GraphQL Query Shape

From `getShopifyManualGiftCardGids` in `@cona/core`:

```mermaid theme={null}
flowchart LR
    A["nodes(ids: giftCardGids)"] --> B["... on GiftCard"]
    B --> C["id"]
    B --> D["initialValue { amount, currencyCode }"]
    B --> E["order { id }"]
    E --> F["lineItems(first: 50)"]
    F --> G["isGiftCard"]
    F --> H["quantity"]
    F --> I["originalUnitPrice"]
    F --> J["discountAllocations"]
    J --> K["allocatedAmount { amount }"]
```

Discount ratio is computed only from lines where `isGiftCard === true`, so
unrelated discounts on other products in the same purchase order do **not**
pollute the ratio.

## Activity → Workflow → Adapter Handoff (Map Serialization)

Temporal JSON-serializes all activity payloads. Native `Map` and `Set`
objects do not survive that round-trip, so we serialize them at the activity
boundary and rehydrate inside the workflow before handing them to the adapter.

```mermaid theme={null}
sequenceDiagram
    participant Core as getShopifyManualGiftCardGids<br/>(@cona/core)
    participant Act as getGiftCardOriginsActivity<br/>(Temporal activity)
    participant WF as api-mode-sync workflow
    participant Helper as enrichResourcesWithGiftCardData
    participant Adapter as shopifyOrdersAdapter

    Core-->>Act: { manualGids: Set, discounts: Map }
    Note over Act: Map → Record<br/>Set → string[]
    Act-->>WF: metadata: {<br/>  manualGiftCardGids: string[],<br/>  purchasedGiftCardDiscounts: Record<string, entry><br/>}
    WF->>Helper: resources + metadata
    Note over Helper: Record → Map<br/>string[] → Set
    Helper-->>WF: resources.raw = {<br/>  manualGiftCardGids: Set,<br/>  purchasedGiftCardDiscounts: Map<br/>}
    WF->>Adapter: transformToDocuments(resources)
    Adapter-->>Adapter: Uses Map/Set natively
```

The `enrichResourcesWithGiftCardData` helper lives in
`packages/temporal-workflows/src/workflows/sync/gift-card-enrichment.ts` and
is deliberately extracted as a pure function so it is unit-testable without
the Temporal runtime.

## Redemption-Time Document Shape

For an order paid with a purchased, discounted gift card, the adapter emits:

1. **Sales invoice** with the normal product lines **plus** one or more
   `gift_card_discount` lines. Each `gift_card_discount` line is split across
   the order's tax rates proportionally to the pre-discount gross value at
   each rate. Example: a €6 discount on an order with €20 @ 19% VAT and €10
   @ 7% VAT becomes two lines: €4 @ 19% and €2 @ 7%.
2. **One `gift_card_payment` document** per gift card transaction, with the
   payment amount **reduced** by the proportional discount (so the payment
   equals the net liability we booked at purchase, not the gross face value).

Manually-issued cards are different — they produce a plain `discount` line
item on the sales invoice and **no** payment document (because there is no
liability to reduce).

```mermaid theme={null}
flowchart TD
    A[Order with gift card payment] --> B{Look up GID in<br/>adapter resources}
    B -->|in manualGids Set| C[Emit type: discount<br/>line on sales invoice]
    B -->|in discounts Map| D[Emit type: gift_card_discount<br/>split by tax rate<br/>on sales invoice]
    B -->|neither<br/>not purchased not manual| E[Full face value<br/>gift_card_payment]
    C --> F[No payment document]
    D --> G[gift_card_payment<br/>amount = faceValue × 1 − discountRatio]
    E --> H[gift_card_payment<br/>amount = faceValue]
```

## Rounding Consistency

`getPurchasedGiftCardDiscountTotal` and `getPurchasedGiftCardDiscountLineItems`
must agree to the cent. Both round **per transaction** (`roundMoney(amount × discountRatio)`)
before summing, because summing unrounded floats and rounding once at the end
would drift against the sum of per-line rounded amounts and trigger spurious
"orderTotal ≠ sum of line items" mismatch warnings.

## Files Involved

| Layer           | File                                                                                | Role                                                                                                            |
| --------------- | ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Core            | `packages/core/src/shopify/get-gift-cards-by-ids.ts`                                | GraphQL lookup, classify manual vs purchased, compute discount ratio                                            |
| Activity        | `packages/temporal-workflows/src/activities/shopify/get-gift-card-origins.ts`       | Temporal wrapper, Map→Record serialization                                                                      |
| Activity        | `packages/temporal-workflows/src/activities/shopify/get-shopify-orders-enriched.ts` | Extracts GIDs from `receiptJson`, calls origins activity                                                        |
| Workflow helper | `packages/temporal-workflows/src/workflows/sync/gift-card-enrichment.ts`            | Rehydrates Set + Map onto adapter resources                                                                     |
| Workflow        | `packages/temporal-workflows/src/workflows/sync/api-mode-sync.ts`                   | Calls the enrichment helper between fetch and transform                                                         |
| Adapter helpers | `packages/temporal-workflows/src/adapters/shopify/orders/helpers.ts`                | `extractGiftCardGidFromReceipt`, `splitDiscountByTaxRate`, gift-card line item + payment builders               |
| Adapter         | `packages/temporal-workflows/src/adapters/shopify/orders/index.ts`                  | Orchestrates line items, payments, and reconciliation                                                           |
| Utils           | `packages/utils/src/shopify-order-transformers.ts`                                  | `processLineItemDiscountAllocations` tags purchase-time discounts as `gift_card_discount` on `isGiftCard` lines |

## Common Pitfalls for Future Devs

* **Don't** compute the discount ratio from the whole purchase order total.
  Other items on the same order can have their own discounts; you must filter
  to `isGiftCard === true` lines first.
* **Don't** try to match gift cards by code, balance, or amount — use the GID
  from `receiptJson`.
* **Don't** pass `Map` or `Set` through a Temporal activity return value —
  they will silently become `{}` after JSON round-trip. Always serialize at the
  boundary and rehydrate in the workflow.
* **Don't** round once at the end when summing per-line discounts — round per
  transaction to stay consistent with line item rounding.
* **Don't** treat manual and purchased cards the same at redemption — they
  hit different accounts (one is a discount/revenue reduction, the other is
  a liability reduction).
