docs(18.1-07): complete webhook and sync state gap closure plan
This commit is contained in:
@@ -0,0 +1,329 @@
|
||||
---
|
||||
phase: 18.1-payment-gateway-subscriptions
|
||||
plan: 07
|
||||
subsystem: payments
|
||||
tags: [stripe, subscriptions, webhook, river, postgres, advisory-lock, gap-closure]
|
||||
|
||||
requires:
|
||||
- phase: 18.1-payment-gateway-subscriptions
|
||||
provides: "plans 02/03 (pgsub core, pgstripe subscriptions plugin and webhook job), plan 06 (unit coverage), 18.1-REVIEW.md CR-01/WR-04/WR-06"
|
||||
provides:
|
||||
- "Fresh-state webhook job: created/updated events apply the subscription Stripe holds (Gateway.RetrieveSubscription), outside any transaction, under a per-subscription session advisory lock, claim-first kept"
|
||||
- "classes.WithSessionLock and classes.SubscriptionLockKey (pg_advisory_lock on one pinned connection)"
|
||||
- "classes.ErrSubscriptionNotStored: a deletion before its subscription is stored is retried"
|
||||
- "Plan resolution by price, then stripe_product_id, then plan_code (contracts.Update.ProductID/PlanCode, SubscriptionArgs product_id/plan_code)"
|
||||
- "contracts.SubscriptionRetriever and a SyncSubscription that writes only gateway-owned columns of a locked, re-read row and refreshes the plan"
|
||||
- "TestFreshStateSmoke (Postgres smoke, 7 subtests)"
|
||||
- "D-21 ordering amendment recorded in both READMEs, 18.1-CONTEXT.md and COVERAGE.md"
|
||||
affects: [18.1-08, 18.1-09, horoskopia-port, pgsub-consumers]
|
||||
|
||||
actuals:
|
||||
tokens: 14061 # chars/4 over the added lines of the realized diff (3 repos)
|
||||
tasks: 3
|
||||
commits: 8 # MEASURED per repo: pgsub 3 + pgstripe 4 + summercms.go 1 (before the SUMMARY commit)
|
||||
commits: 8
|
||||
plan_head_before: "summercms.go 0bd3973b39061b5e510d42cda042d783e3c35ce8; pgsub 472b75bb420675d6a0e547622e9f28383a7092bc; pgstripe baf3c55aa0cba87ae80babbb9b7e0711b9f6e86d"
|
||||
plan_head_after: "summercms.go 3cc198ef9f308908222bac2ad4a6c0c63a303685; pgsub 9d83b789e5c32124a5889b42285efe7b504e44d8; pgstripe 0484d4fa33ceeffe528d240b814c16fa2c739666"
|
||||
|
||||
tech-stack:
|
||||
added: []
|
||||
patterns:
|
||||
- "Session-level advisory lock on a GORM-pinned connection (DB.Connection + Session{NewDB}) around a provider call, then the short transaction on the same connection"
|
||||
- "Optional capability interface (contracts.SubscriptionRetriever) instead of growing the published Gateway interface"
|
||||
- "Select-of-owned-columns update of a FOR UPDATE re-read row instead of a full-row Save"
|
||||
|
||||
key-files:
|
||||
created:
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/classes/lock.go
|
||||
- ../sm-pgstripe-plugin/subscriptions/fresh_state_smoke_test.go
|
||||
modified:
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/classes/apply.go
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/classes/service.go
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/contracts/gateway.go
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/core_smoke_test.go
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/apply_events_test.go
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/service_api_test.go
|
||||
- ../sm-paymentgatewaysubscriptions-plugin/README.md
|
||||
- ../sm-pgstripe-plugin/subscriptions/gateway.go
|
||||
- ../sm-pgstripe-plugin/subscriptions/job.go
|
||||
- ../sm-pgstripe-plugin/subscriptions/decode.go
|
||||
- ../sm-pgstripe-plugin/subscriptions/decode_test.go
|
||||
- ../sm-pgstripe-plugin/subscriptions/job_test.go
|
||||
- ../sm-pgstripe-plugin/subscriptions/host_test.go
|
||||
- ../sm-pgstripe-plugin/subscriptions/README.md
|
||||
- .planning/phases/18.1-payment-gateway-subscriptions/COVERAGE.md
|
||||
- .planning/phases/18.1-payment-gateway-subscriptions/18.1-CONTEXT.md
|
||||
|
||||
key-decisions:
|
||||
- "Re-retrieve from Stripe instead of an event.created watermark (one-second resolution cannot order Checkout's created/updated pair); no migration"
|
||||
- "Stripe call outside any transaction, serialised by a session-level pg_advisory_lock keyed hashtextextended(SubscriptionLockKey(id), 0); a failed lock or unlock discards the pinned connection (driver.ErrBadConn) so the server drops the lock"
|
||||
- "Stripe 404 / resource_missing falls back to the event snapshot with a Warn log (PHP behaviour as the floor)"
|
||||
- "Plan fallback order price, product, plan_code (any plan with the code, active or not)"
|
||||
- "On a gateway without SubscriptionRetriever, SyncSubscription uses only the fields Gateway.SyncSubscription changed on a detached copy, so untouched fields never write the caller's stale values"
|
||||
- "D-21 superseded for event ordering (user decision 2026-10-14, verification human item 1); its other defaults unchanged"
|
||||
|
||||
patterns-established:
|
||||
- "Per-subscription serialisation: every writer of a subscription's gateway state takes classes.WithSessionLock(ctx, db, classes.SubscriptionLockKey(id), ...)"
|
||||
|
||||
requirements-completed: [SC-2, SC-4, D-11, D-21, CR-01, WR-04, WR-06]
|
||||
|
||||
coverage:
|
||||
- id: D1
|
||||
description: "A stale customer.subscription.updated cannot regress an active row; the job applies Stripe's current state under the per-subscription lock (CR-01 sequence 1)"
|
||||
requirement: CR-01
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/fresh_state_smoke_test.go#TestFreshStateSmoke/stale_updated_after_active"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/host_test.go#TestHostSubscriptionFlow"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D2
|
||||
description: "No spurious reactivated after a local cancel at period end and an older update (CR-01 sequence 3)"
|
||||
requirement: CR-01
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/fresh_state_smoke_test.go#TestFreshStateSmoke/stale_update_after_cancel_no_reactivated"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D3
|
||||
description: "Claim-first kept: duplicates answered from the marker, another app's event skipped with the marker kept, failed apply leaves no marker, 404 applies the snapshot (D-11, STRIPE-006/007)"
|
||||
requirement: D-11
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/job_test.go#TestSubscriptionJob"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/php_map_test.go#TestStripeSubscriptionReliability"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/fresh_state_smoke_test.go#TestFreshStateSmoke/missing_at_stripe_applies_snapshot"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D4
|
||||
description: "A deletion before its subscription is stored is retried (ErrSubscriptionNotStored); the late created event stores Stripe's canceled state and activated never fires (CR-01 sequence 2)"
|
||||
requirement: CR-01
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/fresh_state_smoke_test.go#TestFreshStateSmoke/deleted_before_created"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "sm-paymentgatewaysubscriptions-plugin/core_smoke_test.go#TestSubscriptionsCoreSmoke/event_diff"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D5
|
||||
description: "A subscription on an archived price or a renamed/inactive plan is stored (price, product, plan_code resolution; product_id and plan_code in job args, no PII)"
|
||||
requirement: WR-04
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/fresh_state_smoke_test.go#TestFreshStateSmoke/repriced_plan_by_product"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/fresh_state_smoke_test.go#TestFreshStateSmoke/plan_by_code_fallback"
|
||||
status: pass
|
||||
- kind: unit
|
||||
ref: "sm-pgstripe-plugin/subscriptions/decode_test.go#TestJobArgsMinimal"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D6
|
||||
description: "SyncSubscription writes only gateway-owned columns of a locked re-read row, keeps a plan switch and customer id written after the load, refreshes the plan from Stripe, fires nothing, never calls the gateway inside a transaction (WR-06)"
|
||||
requirement: WR-06
|
||||
verification:
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/fresh_state_smoke_test.go#TestFreshStateSmoke/sync_keeps_plan_switch"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "sm-paymentgatewaysubscriptions-plugin/service_api_test.go#TestServiceAPI/sync_subscription_saves_the_gateway_state_without_events"
|
||||
status: pass
|
||||
- kind: integration
|
||||
ref: "sm-pgstripe-plugin/subscriptions/host_test.go#TestHostSubscriptionFlow/sync_subscription"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D7
|
||||
description: "D-21 ordering amendment recorded in both READMEs, 18.1-CONTEXT.md and COVERAGE.md; no route, event, table, column, config key or API shape change; pgsub links no stripe-go"
|
||||
requirement: D-21
|
||||
verification:
|
||||
- kind: other
|
||||
ref: "grep -c 'D-21 amendment' 18.1-CONTEXT.md; go -C ../sm-paymentgatewaysubscriptions-plugin list -deps ./... | grep -c stripe-go (0); bash scripts/check-phase18.1.sh plugins; bash scripts/check-phase18.1.sh postgres"
|
||||
status: pass
|
||||
human_judgment: false
|
||||
- id: D8
|
||||
description: "Concurrency behaviour under real parallel River workers and the advisory lock (lock wait, connection hold during the Stripe call) in a production-like deployment"
|
||||
verification: []
|
||||
human_judgment: true
|
||||
rationale: "The smoke covers sequential stale/retry orderings and two concurrent identical jobs; true interleaving of distinct events under load, and pool behaviour, are not asserted by a test (plan 09 adds unit coverage)"
|
||||
|
||||
duration: 20min
|
||||
completed: 2026-10-16
|
||||
status: complete
|
||||
---
|
||||
|
||||
# Phase 18.1 Plan 07: Webhook and sync state gap closure (CR-01, WR-04, WR-06) Summary
|
||||
|
||||
**The Stripe webhook job now applies the subscription state Stripe holds, which it re-retrieves outside any transaction under a per-subscription Postgres session advisory lock. The claim-first transaction is unchanged. A deletion that arrives before its subscription is stored is retried. Plans resolve by price, then product, then plan code. SyncSubscription writes only the gateway-owned columns of a locked, re-read row and refreshes the plan.**
|
||||
|
||||
## Performance
|
||||
|
||||
- **Duration:** 20 min
|
||||
- **Started:** 2026-10-16T10:04:33Z
|
||||
- **Completed:** 2026-10-16T10:24:17Z
|
||||
- **Tasks:** 3 of 3
|
||||
- **Files modified:** 18 (8 pgsub, 8 pgstripe, 2 planning docs)
|
||||
|
||||
## Accomplishments
|
||||
|
||||
- **CR-01, fresh state.** For `customer.subscription.created` and `.updated` the job:
|
||||
1. checks the marker (`EventProcessed`) before any Stripe call;
|
||||
2. sends another application's event to the claim-first path (marker kept, no Stripe call);
|
||||
3. otherwise takes `classes.WithSessionLock(ctx, db, classes.SubscriptionLockKey(id), ...)`, calls `Gateway.RetrieveSubscription` with no transaction open, then claims and applies in a `lagoon.Transaction` on the same pinned connection.
|
||||
- A Stripe 404 or `resource_missing` (`ErrSubscriptionMissing`) applies the event snapshot with a Warn log.
|
||||
- Any other retrieval error returns with no marker, so River retries.
|
||||
- **CR-01, deletion retry.** `ApplyDeleted` on an unknown row returns an error wrapping `classes.ErrSubscriptionNotStored`, with no change and no event. Deleted events run under the same per-subscription lock. The marker rolls back and River retries. The late `created` event stores Stripe's `canceled`, so `activated` never fires.
|
||||
- **WR-04.** `contracts.Update` gains `ProductID` and `PlanCode`; `SubscriptionArgs` gains `product_id` and `plan_code` (ids and a code only, no PII). `Apply` resolves the plan in this order:
|
||||
1. exact price id;
|
||||
2. `stripe_product_id`;
|
||||
3. plan code, active or not.
|
||||
|
||||
A match by product or code logs at Warn. When nothing matches, the error names the price, product and code.
|
||||
- **WR-06.** `contracts.SubscriptionRetriever` (optional; the Gateway contract keeps its eight methods). `Service.SyncSubscription`:
|
||||
- runs under the same lock and asks the gateway outside any transaction;
|
||||
- re-reads the row `FOR UPDATE` and saves with `Select` of only the gateway-owned columns, plus `subscription_plan_id` from the retrieved price on the retriever path;
|
||||
- copies the saved row into the caller's `*sub` and fires no events.
|
||||
- **D-21 amendment** recorded in both READMEs ("Differences from PHP" and "Deviations from PHP"), `18.1-CONTEXT.md` § Amendments and `COVERAGE.md`.
|
||||
|
||||
## Task Commits
|
||||
|
||||
sm-paymentgatewaysubscriptions-plugin (pgsub), committed first per task:
|
||||
1. Task 1: `9165dd8` feat(18.1-07): per-subscription session advisory lock (WithSessionLock, SubscriptionLockKey)
|
||||
2. Task 2: `89c5f1b` feat(18.1-07): deletion before creation is retried; plan resolution by price, product, plan code (CR-01, WR-04)
|
||||
3. Task 3: `9d83b78` feat(18.1-07): SyncSubscription writes only gateway-owned columns of a locked, fresh row and refreshes the plan (WR-06)
|
||||
|
||||
sm-pgstripe-plugin (pgstripe):
|
||||
1. Task 1 (tracer): `f06f67d` feat(18.1-07): webhook job applies the subscription state Stripe holds (CR-01)
|
||||
2. Task 2: `2a86d7c` feat(18.1-07): product and plan code in job arguments, deletions under the subscription lock (CR-01, WR-04)
|
||||
3. Task 3: `5f9bd1a` feat(18.1-07): Stripe gateway is a SubscriptionRetriever; SyncSubscription built on RetrieveSubscription (WR-06)
|
||||
4. Task 3 follow-up: `0484d4f` docs(18.1-07): name the retriever interface once in gateway.go (comment only, so the acceptance grep finds exactly one line)
|
||||
|
||||
summercms.go (planning docs):
|
||||
- Task 3: `3cc198e` docs(18.1-07): D-21 ordering amendment and the fresh-state retrieval in COVERAGE
|
||||
|
||||
Nothing was pushed or tagged (release is plan 09).
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `sm-paymentgatewaysubscriptions-plugin/classes/lock.go` (new): `WithSessionLock`, `SubscriptionLockKey`, `lockKeyPrefix`; `discardConn` drops the pinned connection when the lock or unlock call fails.
|
||||
- `.../classes/apply.go`: `ErrSubscriptionNotStored`; `resolvePlan` (service method with Warn logging, plus the package function), `planForProduct`, `planByCode`; Apply and ApplyDeleted use them; doc comments updated.
|
||||
- `.../classes/service.go`: `SyncSubscription` rewritten; `gatewayFields`, `changedGatewayFields`, `detached`.
|
||||
- `.../contracts/gateway.go`: `Update.ProductID`, `Update.PlanCode`, `SubscriptionRetriever`.
|
||||
- `sm-pgstripe-plugin/subscriptions/gateway.go`: `ErrSubscriptionMissing`, `RetrieveSubscription`, `SyncSubscription` on top of it, the `SubscriptionRetriever` assertion, `MetadataPlanCode` in the checkout metadata.
|
||||
- `.../subscriptions/job.go`: `work` dispatch, `freshState`, `foreignApp`, `claimAndRoute`, `applySubscription`; deletions under the lock; `SubscriptionArgs.ProductID` and `PlanCode`; `update()` maps both.
|
||||
- `.../subscriptions/decode.go`: `MetadataPlanCode`, `subscriptionItem.Price.Product`, `productID()`, `args()` fills both new fields.
|
||||
- `.../subscriptions/fresh_state_smoke_test.go` (new): `TestFreshStateSmoke`.
|
||||
- READMEs of both plugins, `COVERAGE.md`, `18.1-CONTEXT.md`.
|
||||
|
||||
## Adapted existing assertions
|
||||
|
||||
| Repo / file | Test | Old expectation | New expectation | Why |
|
||||
|---|---|---|---|---|
|
||||
| pgstripe `subscriptions/host_test.go` | helper `subEvent` (all users of it) | the fake held no object for a delivered event | `subEvent` stores the event's object in the fake, so the host flow runs the fresh path | real Stripe holds the state it sends (plan Task 1) |
|
||||
| pgstripe `subscriptions/host_test.go` | `TestHostSubscriptionFlow/pre_basil_periods` | the pre-basil event built with `eventBody` was applied as is | the fake is seeded with the same pre-basil object before delivery; the period assertion is unchanged | the job now applies what Stripe holds; without the seed the fake held the activation's periods |
|
||||
| pgsub `core_smoke_test.go` | `TestSubscriptionsCoreSmoke/event_diff` (`sub_never_seen` deletion) | `env.tx` with nil error, nil row, no event | `env.txErr` returns an error wrapping `classes.ErrSubscriptionNotStored` naming the id; nil row, no event | CR-01 sequence 2 |
|
||||
| pgsub `apply_events_test.go` | `TestApplyEvents` (deleted matrix, unknown ids) | `sub_never_seen` and `""` both nil, nil | `sub_never_seen` returns `ErrSubscriptionNotStored`; `""` stays nil, nil; neither changes a row or fires an event | CR-01 sequence 2 |
|
||||
| pgstripe `subscriptions/job_test.go` | `TestSubscriptionJob/stripe_007 skips keep the marker and fire nothing` | `evt_job_del_unknown` returned nil and kept 1 marker | Work returns `ErrSubscriptionNotStored` and leaves 0 markers (moved out of the skip loop); doc comment updated | CR-01 sequence 2 |
|
||||
| pgstripe `subscriptions/decode_test.go` | `TestJobArgsMinimal` | subscription key list without `plan_code`/`product_id`; `plan_code` in the leak list for every type | key list gains `plan_code` and `product_id` (fixture price gains `product`); `plan_code` stays a leak for invoice types only; values asserted | WR-04 (plan Task 2) |
|
||||
| pgstripe `subscriptions/decode_test.go` | `TestDecodeSubscription` (basil args) | whole-struct `want` without `ProductID`/`PlanCode` | fixture price carries an expanded `product` object; `want` gains `ProductID: "prod_pro"`, `PlanCode: "pro"` | WR-04 (the fixture already had `plan_code` metadata) |
|
||||
| pgsub `service_api_test.go` | `TestServiceAPI/sync subscription saves the gateway state without events` | rows without a gateway subscription id were synced | rows get `sub_probe_sync` / `sub_probe_sync_fail`; adds checks that the caller's copy is refreshed, the probe saw no open transaction, and a row without an id is refused | WR-06: the new SyncSubscription needs the id it locks and reports on |
|
||||
|
||||
## Smoke results (Docker host available)
|
||||
|
||||
- Task 1: `TestFreshStateSmoke`, `TestHostSubscriptionFlow`, `TestSubscriptionJob`, `TestStripeSubscriptionReliability`, `TestSubscriptionWebhookSmoke`: 5 top-level PASS.
|
||||
- Task 2: full pgsub suite green; `TestFreshStateSmoke`, `TestHostSubscriptionFlow`, `TestSubscriptionJob`, `TestStripeSubscriptionReliability`: 4 top-level PASS.
|
||||
- Task 3: full Postgres suites green in both repositories, and `bash scripts/check-phase18.1.sh plugins` passed. As an extra check, `bash scripts/check-phase18.1.sh postgres` also passed (14 pgsub, 4 + 7 pgstripe, 3 cabana required suites).
|
||||
- `TestFreshStateSmoke` subtests, all PASS:
|
||||
- `stale_updated_after_active`
|
||||
- `stale_update_after_cancel_no_reactivated`
|
||||
- `missing_at_stripe_applies_snapshot`
|
||||
- `deleted_before_created`
|
||||
- `repriced_plan_by_product`
|
||||
- `plan_by_code_fallback`
|
||||
- `sync_keeps_plan_switch`
|
||||
- The host flow ran 17 events through the fresh retrieval and 1 through the snapshot fallback (`checkout_activation`, whose event is built without fake state).
|
||||
- D-06: `go list -deps` of pgsub contains no stripe-go.
|
||||
|
||||
## D-21 amendment text (18.1-CONTEXT.md § Amendments)
|
||||
|
||||
> **D-21 amendment (2026-10-14, gap closure 18.1-07, user decision on verification human item 1):** out-of-order events no longer behave as PHP; created and updated events apply the subscription Stripe holds, serialised per subscription, and a deletion before its subscription is stored is retried. D-21's other defaults are unchanged.
|
||||
|
||||
## Decisions Made
|
||||
|
||||
- The plan's discretionary choices were followed: re-retrieve rather than a watermark, a session lock outside any transaction, the 404 snapshot fallback, the price/product/code order, and an optional interface rather than a ninth Gateway method.
|
||||
- A failed `pg_advisory_lock` or `pg_advisory_unlock` (or an unlock reporting that the lock was not held) marks the pinned `*sql.Conn` bad (`Raw` returning `driver.ErrBadConn`). database/sql then closes it instead of returning it to the pool, and closing the session releases any lock it holds. Without this, a lock granted at the moment of a cancellation could leak onto a pooled connection.
|
||||
- On a gateway without `SubscriptionRetriever`, `SyncSubscription` uses only the gateway-owned fields that `Gateway.SyncSubscription` changed on a detached copy. When the cancel flag is unchanged, the re-read row keeps its own value. Copying every field would write the caller's stale values, which is WR-06 again.
|
||||
- `RetrieveSubscription` refuses an answer whose `id` differs from the requested one, and `SyncSubscription` refuses an Update naming another subscription.
|
||||
|
||||
## Deviations from Plan
|
||||
|
||||
### Auto-fixed Issues
|
||||
|
||||
**1. [Rule 3 - Blocking] Adapted assertions in test files outside the plan's file list**
|
||||
- **Found during:** Tasks 2 and 3
|
||||
- **Issue:** `apply_events_test.go`, `job_test.go` and `service_api_test.go` encoded the replaced behaviour: nil, nil for an unknown deletion, and a sync of a row without a gateway subscription id. `TestDecodeSubscription` compares the whole args struct.
|
||||
- **Fix:** adapted in the same commits, listed in the table above.
|
||||
- **Files modified:** pgsub `apply_events_test.go`, `service_api_test.go`; pgstripe `subscriptions/job_test.go`, `subscriptions/decode_test.go` (the last is in the plan list, but only for `TestJobArgsMinimal`).
|
||||
- **Committed in:** `89c5f1b`, `2a86d7c`, `9d83b78`
|
||||
|
||||
**2. [Rule 2 - Missing critical] Connection discard on a failed lock or unlock**
|
||||
- **Found during:** Task 1
|
||||
- **Issue:** GORM's `Connection` returns the pinned connection to the pool on close. A session lock still held there would block every later job of that subscription.
|
||||
- **Fix:** `discardConn` marks it bad (`driver.ErrBadConn`) so it is closed (threat T-18.1-39).
|
||||
- **Files modified:** `classes/lock.go`
|
||||
- **Committed in:** `9165dd8`
|
||||
|
||||
**3. [Rule 1 - Bug] Non-retriever sync path would rewrite stale values**
|
||||
- **Found during:** Task 3
|
||||
- **Issue:** the plan's "derive an Update from the copy's gateway-owned fields" would write the caller's stale periods, cancel flag and customer id whenever the gateway left them untouched.
|
||||
- **Fix:** only fields that changed relative to the caller's load are used, on a deep copy of the pointer fields.
|
||||
- **Files modified:** `classes/service.go`
|
||||
- **Committed in:** `9d83b78`
|
||||
|
||||
**4. [Scope, additive] Extra smoke subtests**
|
||||
- `stale_update_after_cancel_no_reactivated` (CR-01 sequence 3, a must-have truth) and `plan_by_code_fallback` (the WR-04 code step) were added next to the five named subtests.
|
||||
- Each subtest name appears only in its `t.Run`, so the acceptance greps print the exact line counts. The doc comment uses prose names.
|
||||
|
||||
**5. [Acceptance grep] Comment-only follow-up commit `0484d4f`**
|
||||
- `grep -n "contracts.SubscriptionRetriever" gateway.go` must print one line, and the `SyncSubscription` doc comment repeated the name. The comment was reworded.
|
||||
|
||||
---
|
||||
|
||||
**Total deviations:** 3 auto-fixed (1 blocking, 1 missing critical, 1 bug) plus 2 additive or cosmetic. **Impact:** all correctness-preserving; no public route, event, table, column, config key or response shape changed.
|
||||
|
||||
## Issues Encountered
|
||||
|
||||
- Cross-repository ordering: pgstripe resolves pgsub through a `replace` onto the working tree, and the plan orders pgsub first. So between the pgsub Task 2 commit (`89c5f1b`) and the pgstripe Task 2 commit (`2a86d7c`), pgstripe's checked-in `TestSubscriptionJob` still expected the old unknown-deletion answer. The pgstripe change was prepared and tested before the pgsub commit, and the two were committed back to back. Every pair of commits that belong together is green.
|
||||
|
||||
## Observations for later plans
|
||||
|
||||
- While the job holds the lock it holds one pinned connection, plus a short second one for `LoadSettings` inside `RetrieveSubscription`. lagoon sets no pool cap and conga runs 4 workers per queue, so this cannot starve by default. An application that caps `MaxOpenConns` at or below its worker count could. Plan 09 may want a note or test.
|
||||
- `checkout_activation` in `TestHostSubscriptionFlow` still exercises the snapshot fallback (no fake state). That is deliberate; the fresh path is covered by the `subEvent`-based subtests and `TestFreshStateSmoke`.
|
||||
|
||||
## Threat Flags
|
||||
|
||||
None. No new endpoint, auth path or trust-boundary schema change. The new job argument keys (`product_id`, `plan_code`) are T-18.1-40 (accepted), and `TestJobArgsMinimal` pins them.
|
||||
|
||||
## User Setup Required
|
||||
|
||||
None.
|
||||
|
||||
## Next Phase Readiness
|
||||
|
||||
- Ready for 18.1-08 (gap closure 2 of 3) and 18.1-09, which owns the unit coverage of the lock, `resolvePlan`, `gatewayFields` and `RetrieveSubscription`, and the release and tags.
|
||||
- No blockers.
|
||||
|
||||
---
|
||||
*Phase: 18.1-payment-gateway-subscriptions*
|
||||
*Completed: 2026-10-16*
|
||||
|
||||
## Self-Check: PASSED
|
||||
|
||||
Created files exist (lock.go, fresh_state_smoke_test.go, this SUMMARY); all 8 task commits found (pgsub 9165dd8 89c5f1b 9d83b78, pgstripe f06f67d 2a86d7c 5f9bd1a 0484d4f, summercms.go 3cc198e); every acceptance criterion of Tasks 1-3 re-run and passing; both plugin trees clean for the plan paths.
|
||||
Reference in New Issue
Block a user