Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,19 @@

## Unreleased

- Local evidence objects — coupons, promotion codes, subscription schedules,
disputes, tax rates, tax IDs and customer cash balances — are now stored in
the run's own database instead of process memory. They were the only objects
that did not survive a restart, so a restarted server kept answering with the
rest of its data while every lookup that needed one of them failed: a
subscription whose `default_tax_rates` referenced a tax rate created before
the restart returned a `resource_missing` error, with nothing in the
surviving data to suggest why. Runs backed by memory stay ephemeral, which
is what they were always for. Idempotency keys remain in memory on purpose —
losing them on restart is the Stripe-like behaviour.
- `TestSQLiteMigrationsRun` now derives the expected versions from the embedded
migration files rather than a hand-written list, so it no longer needs an edit
per migration and it fails on a gap or a duplicated number.
- `POST /v1/invoices/{id}/void` moves an `open` invoice to `void`, records
`billtap_voided_at`, and emits `invoice.voided`. Other statuses return
`invalid_request_error` with `status must be open`.
Expand Down
29 changes: 16 additions & 13 deletions internal/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ type Options struct {
Webhooks *webhooks.Service
Diagnostics *diagnostics.Service
PublicBaseURL string
// LocalEvidence persists coupons, promotion codes, schedules, disputes, tax
// rates, tax IDs and cash balances. Nil keeps them in memory only.
LocalEvidence LocalEvidenceRepository
}

type Handler struct {
Expand Down Expand Up @@ -63,7 +66,7 @@ func New(opts Options) http.Handler {
publicBase: strings.TrimRight(opts.PublicBaseURL, "/"),
mux: http.NewServeMux(),
idem: newIdempotencyStore(),
local: newLocalEvidenceStore(),
local: newLocalEvidenceStore(opts.LocalEvidence),
compat: stripecompat.DefaultRegistry(),
knownRoutes: stripecompat.DefaultRouteCatalog(),
validation: stripecompat.DefaultValidationCatalog(),
Expand Down Expand Up @@ -4825,9 +4828,9 @@ func (h *Handler) applyFixtureDisputes(r *http.Request, pack fixtures.Pack) ([]m
out := make([]map[string]any, 0, len(pack.Disputes))
for _, fixture := range pack.Disputes {
dispute := disputeFixturePayload(fixture)
h.local.mu.Lock()
h.local.disputes[fmt.Sprint(dispute["id"])] = dispute
h.local.mu.Unlock()
if err := h.local.save(kindDispute, fmt.Sprint(dispute["id"]), dispute); err != nil {
return nil, err
}
out = append(out, cloneEvidence(dispute))
h.emitGenericWebhook(r, "charge.dispute.created", fmt.Sprint(dispute["id"]), dispute, webhooks.SourceFixture)
if fmt.Sprint(dispute["status"]) != "needs_response" {
Expand Down Expand Up @@ -4886,9 +4889,9 @@ func (h *Handler) applyFixtureTaxRates(pack fixtures.Pack) ([]map[string]any, er
out := make([]map[string]any, 0, len(pack.TaxRates))
for _, fixture := range pack.TaxRates {
taxRate := taxRateFixturePayload(fixture)
h.local.mu.Lock()
h.local.taxRates[fmt.Sprint(taxRate["id"])] = taxRate
h.local.mu.Unlock()
if err := h.local.save(kindTaxRate, fmt.Sprint(taxRate["id"]), taxRate); err != nil {
return nil, err
}
out = append(out, cloneEvidence(taxRate))
}
return out, nil
Expand Down Expand Up @@ -4936,9 +4939,9 @@ func (h *Handler) applyFixtureCoupons(pack fixtures.Pack) ([]map[string]any, err
out := make([]map[string]any, 0, len(pack.Coupons))
for _, fixture := range pack.Coupons {
coupon := couponFixturePayload(fixture)
h.local.mu.Lock()
h.local.coupons[fmt.Sprint(coupon["id"])] = coupon
h.local.mu.Unlock()
if err := h.local.save(kindCoupon, fmt.Sprint(coupon["id"]), coupon); err != nil {
return nil, err
}
out = append(out, cloneEvidence(coupon))
}
return out, nil
Expand Down Expand Up @@ -5007,9 +5010,9 @@ func (h *Handler) applyFixturePromotionCodes(pack fixtures.Pack) ([]map[string]a
if err != nil {
return nil, err
}
h.local.mu.Lock()
h.local.promotionCodes[fmt.Sprint(promo["id"])] = promo
h.local.mu.Unlock()
if err := h.local.save(kindPromotionCode, fmt.Sprint(promo["id"]), promo); err != nil {
return nil, err
}
out = append(out, cloneEvidence(promo))
}
return out, nil
Expand Down
5 changes: 5 additions & 0 deletions internal/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7509,6 +7509,11 @@ func newTestHandlerWithOptions(t *testing.T, opts Options) http.Handler {
webhookService := webhooks.NewService(store)
opts.Webhooks = webhookService
opts.Diagnostics = diagnostics.NewService(store)
if opts.LocalEvidence == nil {
// Wire it by default so the whole suite runs against the persisted path,
// which is what the server does.
opts.LocalEvidence = store
}
// After t.TempDir(): LIFO runs wait+close before TempDir removal.
webhookstest.RegisterStoreCleanup(t, webhookService, store)
return New(opts)
Expand Down
Loading
Loading