← back to Dw Signup Fulfillment

verification/tk11361/FINDING.md

191 lines

# TK-11361 — Duplicate ACTIVE automatic discounts on the DW Free Samples function

**Read-only investigation, 2026-09-10, claude-run-10836. Nothing modified.**

## Status: CONFIRMED (a prior retraction was itself the wrong read)

Two **ACTIVE** `DiscountAutomaticApp` nodes are bound to the *same* Shopify
Function `01a0475d-d202-743e-bdc8-7a659f6ff512` ("DW Free Samples"), both owned
by the `dw-signup-fulfillment` app:

| node | title | created | discountClass | combinesWith (order / product / shipping) |
|---|---|---|---|---|
| `1464777277491` | DW Free Samples (auto) | 2026-08-28T14:01:48Z | PRODUCT | true / **true** / true |
| `1465258770483` | DW Free Samples | 2026-09-02T19:07:32Z | PRODUCT | true / **true** / false |

Neither carries any configuration metafield (`metafields(first:20)` → 0 on
both), so both run the same self-contained function logic. They are
functionally identical apart from `shippingDiscounts`.

## Why an earlier pass "could not reproduce" this — resolved

A concurrent session recorded this finding as **NOT REPRODUCIBLE**, reporting
that a live read returned "exactly one node and zero app discounts." That
retraction was wrong, and the cause is a **query-shape artifact, not stale
data**:

```
automaticDiscountNodes(first:50)  ->  1 node   (DiscountAutomaticBasic "15 test discount", EXPIRED)
discountNodes(first:100)          ->  28 nodes, of which 5 are DiscountAutomaticApp
discountNode(id: <either node>)   ->  resolves, ACTIVE, full detail
```

`automaticDiscountNodes` is the legacy connection and **does not return
app/function-backed automatic discounts**. `discountNodes` (modern) returns all
five, including both DW Free Samples nodes. All three reads above were taken in
the same session with the same full-access token (`…2ea5`), minutes apart.

**Rule of thumb for this store: use `discountNodes`, never
`automaticDiscountNodes`, when app/function discounts matter.**

This is the third bad read on this trail ("0 webhooks", "no customer record",
now "no app discounts") — but unlike the first two it is not staleness, it is
querying the wrong connection. Worth remembering separately.

## Risk — real but UNQUANTIFIED

Both are `discountClass: PRODUCT` with `combinesWith.productDiscounts: true`,
so **they are permitted to stack with each other**. The function gates the
retail entitlement on the customer metafield `custom.free_samples_used < 3`
(see `verification/tk11283/live-snippets__dw-samples-banner.liquid`).

If Shopify evaluates the function once per discount node, the plausible
failure is **6 free samples granted instead of 3**, or the same sample lines
discounted twice. If Shopify dedupes by function, the impact is nil and this
is only housekeeping.

**I could not determine which, read-only.** Distinguishing them requires
exercising a real sample cart at checkout, which is a live customer-facing
surface — not something to fire unattended.

## What is NOT affected
Kelly Paradis / TK-10836 does not depend on this path at all — she was honored
with code **3FREE** (`DiscountCodeBasic`, $15 off, once-per-customer), which is
a separate mechanism from this function. TK-10836's closure stands regardless
of how this resolves.

## Next steps (both gated)
1. **Determine actual stacking behavior** — run one controlled sample cart to
   checkout with a test customer at `free_samples_used = 0` and observe whether
   3 or 6 samples come out free. Live checkout surface → Steve's call.
2. **If duplicate confirmed** — archive the redundant node (almost certainly
   `1465258770483`, the 09-02 re-creation; `scripts/production-validation.js`
   asserts on `'DW Free Samples (auto)'`, the 08-28 one, so that is the node
   the codebase treats as canonical). `discountAutomaticDelete` is a
   customer-facing money-surface write → GATED, memo drafted.

## Reproduce
```bash
TOK=$(grep -h '^SHOPIFY_FULL_ACCESS_TOKEN=' ~/Projects/secrets-manager/.env | cut -d= -f2-)
curl -s -X POST "https://designer-laboratory-sandbox.myshopify.com/admin/api/2024-10/graphql.json" \
  -H "X-Shopify-Access-Token: $TOK" -H 'Content-Type: application/json' \
  -d '{"query":"{ discountNodes(first:100){ nodes { id discount { __typename ... on DiscountAutomaticApp { title status combinesWith{orderDiscounts productDiscounts shippingDiscounts} appDiscountType { functionId } } } } } }"}'
```

Cost: $0 (read-only Shopify Admin GraphQL).

---

# RESOLUTION — Option A measurement, 2026-09-10 (Steve approved "a")

## Answer: **3, not 6.** No over-entitlement, no double-discount, no money impact.

Measured against **real orders** rather than a synthetic cart — a stronger test,
and it required no writes at all (no test customer was ever created).

### Method — natural experiment across the duplicate's introduction
The second node appeared 2026-09-02. So orders after that date have run with
**both** nodes ACTIVE. Compare them to the window before either node existed.

| window | orders sampled | max "Free sample" applications on one order | lines discounted beyond their own price |
|---|---|---|---|
| 2026-07-15 → 2026-08-27 (**neither** DW node existed) | 150 | **3** | 0 |
| 2026-08-28 → 2026-09-10 (both nodes ACTIVE from 09-02) | 100 | **3** | 0 |

Per-order distribution in the both-nodes-active window: 0 apps → 31 orders,
1 → 28, 2 → 13, **3 → 28, and 4+ → zero orders.** The cap held perfectly.

Line-level spot check, order `#33053` (2026-09-10, i.e. both nodes live):
```
3 x AutomaticDiscountApplication  EACH / LINE_ITEM / ENTITLED
line: Crushed Coral - Bordeaux Vegan Leather   qty1  orig $4.25  disc $4.25
line: Crushed Coral - Lavender Vegan Leather   qty1  orig $4.25  disc $4.25
line: Crushed Coral - Maroon   Vegan Leather   qty1  orig $4.25  disc $4.25
order total: $0.00
```
Each line discounted exactly its own price — once, not twice.

## Why it is harmless — the real reason (and it is not what we assumed)

**No evidence that either DW node contributes to any observed order. Observed
behaviour is fully explained by a different app.** (This is strong inference from
title attribution + chronology, NOT a hard provenance link — the Admin API does
not expose the source node id on a discount application. See *Provenance
caveat* below.)

The free samples customers actually receive come from a *different* app:

| node | title | created | function | **app** |
|---|---|---|---|---|
| 1374384455731 | Free discount | 2024-07-25 | `cd0a910b` Amount off products | **Regios Discounts** |
| 1374384881715 | 15% discount | 2024-07-25 | `cd0a910b` Amount off products | **Regios Discounts** |
| 1448857010227 | Cowlendar Discount | 2026-02-18 | `c0df41d5` | Cowlendar Booking |
| 1464777277491 | DW Free Samples (auto) | 2026-08-28 | `01a0475d` DW Free Samples | dw-signup-fulfillment |
| 1465258770483 | DW Free Samples | 2026-09-02 | `01a0475d` DW Free Samples | dw-signup-fulfillment |

The `"Free sample"` applications on real orders are **Regios** output — proven by
the fact that they appear **12 times between 2026-07-15 and 2026-08-27**, before
either DW node existed. Regios has been the live free-samples engine since 2024
and is correctly enforcing the 3-sample cap.

This matches `DEPLOY.md` → *"Retired alternates (NOT wired — reference only) …
`lib/retail-code.js` — shared function-backed code. Needs the 'DW Free Samples'
Shopify Function (Plus + cross-app ownership issues)"* and *"the exact reason
Option C uses variant-scoped Regios instead."*

**Both DW nodes are orphans of a retired path.**

## Revised risk: not a money bug — a latent landmine

Downgraded from "possible 6-free-samples over-entitlement" to **cleanup**.
But it is not nothing: both nodes are ACTIVE with
`combinesWith.productDiscounts: true`. They are inert only because the function
behind them grants nothing today. **If anyone ever repairs or redeploys function
`01a0475d`, two nodes go live at once** and the stacking question becomes real
and immediate — on a money surface, with no canary watching.

## Revised recommendation
Archive **both** DW Free Samples nodes (not just the duplicate) — the path is
retired per DEPLOY.md and Regios owns the live entitlement. This removes the
landmine rather than halving it. Reversible: either node can be recreated from
function id `01a0475d`.

Caveat before firing: `scripts/production-validation.js` asserts on
`TITLE = 'DW Free Samples (auto)'` and would start failing — that assertion
needs updating in the same change.

Still a Shopify write on a customer-facing money surface → **stays gated.**

## Provenance caveat (raised by the codex second-model pass)
Attribution rests on discount-application *titles* plus the chronology, not on a
source-node identifier — Shopify's `AutomaticDiscountApplication` does not expose
which node/function emitted it. So the precise claim is:

- **Proven by outcome:** in the measured window, the entitlement stayed capped at
  3 and no line was over-discounted. **This holds even if a DW node also emits an
  application titled `"Free sample"`** — identical titles could hide a dual
  source, but the *counts and amounts* would still have moved, and they did not.
- **Strongly inferred, not proven:** that Regios is the sole contributor and the
  DW nodes contribute nothing.

Also note the measurement is scoped to the observed window (250 orders,
2026-07-15 → 2026-09-10), not a universal claim.

To close the loop definitively, a targeted edge-case cart (4+ eligible sample
lines, a customer near the lifetime cap, mixed eligible/ineligible variants)
would be needed — that touches live checkout and stays gated.

## Cost
$0. All read-only Shopify Admin GraphQL. No customer created, no cart, no order,
no discount modified.