Skip to content

Commit f1eac14

Browse files
authored
feat: scope credit grant idempotency key to customer (#4716)
1 parent 8123cc6 commit f1eac14

11 files changed

Lines changed: 793 additions & 765 deletions

File tree

api/spec/packages/aip-client-javascript/src/models/types.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3429,8 +3429,9 @@ export interface CreateCreditGrantRequest {
34293429
/**
34303430
* Idempotency key for the credit grant creation request.
34313431
*
3432-
* When provided, reusing the same key returns an HTTP 409 Conflict instead of
3433-
* creating a duplicate grant, which makes create requests safe to retry.
3432+
* Unique per customer: reusing the same key for the same customer returns an HTTP
3433+
* 409 Conflict instead of creating a duplicate grant, which makes create requests
3434+
* safe to retry. The same key may be reused across different customers.
34343435
*/
34353436
key?: string
34363437
}
@@ -3493,8 +3494,9 @@ export interface CreditGrant {
34933494
/**
34943495
* Idempotency key for the credit grant creation request.
34953496
*
3496-
* When provided, reusing the same key returns an HTTP 409 Conflict instead of
3497-
* creating a duplicate grant, which makes create requests safe to retry.
3497+
* Unique per customer: reusing the same key for the same customer returns an HTTP
3498+
* 409 Conflict instead of creating a duplicate grant, which makes create requests
3499+
* safe to retry. The same key may be reused across different customers.
34983500
*/
34993501
key?: string
35003502
/**
@@ -5944,8 +5946,9 @@ export interface CreateCreditGrantRequestInput {
59445946
/**
59455947
* Idempotency key for the credit grant creation request.
59465948
*
5947-
* When provided, reusing the same key returns an HTTP 409 Conflict instead of
5948-
* creating a duplicate grant, which makes create requests safe to retry.
5949+
* Unique per customer: reusing the same key for the same customer returns an HTTP
5950+
* 409 Conflict instead of creating a duplicate grant, which makes create requests
5951+
* safe to retry. The same key may be reused across different customers.
59495952
*/
59505953
key?: string
59515954
}
@@ -6008,8 +6011,9 @@ export interface CreditGrantInput {
60086011
/**
60096012
* Idempotency key for the credit grant creation request.
60106013
*
6011-
* When provided, reusing the same key returns an HTTP 409 Conflict instead of
6012-
* creating a duplicate grant, which makes create requests safe to retry.
6014+
* Unique per customer: reusing the same key for the same customer returns an HTTP
6015+
* 409 Conflict instead of creating a duplicate grant, which makes create requests
6016+
* safe to retry. The same key may be reused across different customers.
60136017
*/
60146018
key?: string
60156019
/**

api/spec/packages/aip/src/customers/credits/grant.tsp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -209,8 +209,9 @@ model CreditGrant {
209209
/**
210210
* Idempotency key for the credit grant creation request.
211211
*
212-
* When provided, reusing the same key returns an HTTP 409 Conflict instead of
213-
* creating a duplicate grant, which makes create requests safe to retry.
212+
* Unique per customer: reusing the same key for the same customer returns an HTTP
213+
* 409 Conflict instead of creating a duplicate grant, which makes create requests
214+
* safe to retry. The same key may be reused across different customers.
214215
*/
215216
@visibility(Lifecycle.Read, Lifecycle.Create)
216217
key?: Shared.ExternalResourceKey;

api/v3/api.gen.go

Lines changed: 737 additions & 734 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v3/client/models_customers.go

Lines changed: 6 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/v3/openapi.yaml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5440,8 +5440,9 @@ components:
54405440
description: |-
54415441
Idempotency key for the credit grant creation request.
54425442
5443-
When provided, reusing the same key returns an HTTP 409 Conflict instead of
5444-
creating a duplicate grant, which makes create requests safe to retry.
5443+
Unique per customer: reusing the same key for the same customer returns an HTTP
5444+
409 Conflict instead of creating a duplicate grant, which makes create requests
5445+
safe to retry. The same key may be reused across different customers.
54455446
expires_at:
54465447
allOf:
54475448
- $ref: '#/components/schemas/DateTime'
@@ -9432,8 +9433,9 @@ components:
94329433
description: |-
94339434
Idempotency key for the credit grant creation request.
94349435
9435-
When provided, reusing the same key returns an HTTP 409 Conflict instead of
9436-
creating a duplicate grant, which makes create requests safe to retry.
9436+
Unique per customer: reusing the same key for the same customer returns an HTTP
9437+
409 Conflict instead of creating a duplicate grant, which makes create requests
9438+
safe to retry. The same key may be reused across different customers.
94379439
additionalProperties: false
94389440
description: CreditGrant create request.
94399441
CreateCreditGrantTaxConfig:

e2e/customer_credits_v3_test.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -134,14 +134,18 @@ func TestV3CreateCreditGrantIdempotencyKey(t *testing.T) {
134134
c.requireStatus(http.StatusCreated, err)
135135
})
136136

137-
t.Run("the same key conflicts across different customers in a namespace", func(t *testing.T) {
137+
t.Run("the same key is allowed across different customers", func(t *testing.T) {
138138
key := ulid.Make().String()
139139

140-
_, err := c.Customers.Credits.Grants.Create(t.Context(), createCustomer("credit_grant_idem_cust_a"), grant(&key))
140+
grantA, err := c.Customers.Credits.Grants.Create(t.Context(), createCustomer("credit_grant_idem_cust_a"), grant(&key))
141141
c.requireStatus(http.StatusCreated, err)
142+
require.NotNil(t, grantA)
142143

143-
_, err = c.Customers.Credits.Grants.Create(t.Context(), createCustomer("credit_grant_idem_cust_b"), grant(&key))
144-
requireProblem(t, err, http.StatusConflict)
144+
grantB, err := c.Customers.Credits.Grants.Create(t.Context(), createCustomer("credit_grant_idem_cust_b"), grant(&key))
145+
c.requireStatus(http.StatusCreated, err)
146+
require.NotNil(t, grantB)
147+
148+
require.NotEqual(t, grantA.ID, grantB.ID)
145149
})
146150

147151
t.Run("an over-length key is rejected with 400", func(t *testing.T) {
@@ -331,8 +335,8 @@ func TestV3VoidCreditGrantPaymentAdjustmentNone(t *testing.T) {
331335

332336
// TestV3CreditGrantKeyReadAndFilter verifies that the idempotency key is exposed
333337
// on the get/list read responses and that list credit grants can be filtered by
334-
// key. The key column carries a unique partial index per namespace, so an
335-
// equality filter targets at most one grant.
338+
// key. The key column carries a unique partial index per namespace and customer,
339+
// so a customer-scoped equality filter targets at most one grant.
336340
func TestV3CreditGrantKeyReadAndFilter(t *testing.T) {
337341
c := newV3Client(t)
338342
currency := v3sdk.BillingCurrencyCode("USD")

openmeter/ent/db/migrate/schema.go

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

openmeter/ent/schema/chargescreditpurchase.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ func (ChargeCreditPurchase) Indexes() []ent.Index {
125125
return []ent.Index{
126126
index.Fields("tax_code_id").
127127
StorageKey("chargecreditpurchases_tax_code_id"),
128-
// Idempotency key, unique per namespace. Partial so it is enforced only while live:
129-
// NULL means no idempotency requested, and a soft-deleted grant must not permanently
130-
// reserve a key the caller may reuse.
131-
index.Fields("namespace", "key").
128+
// Idempotency key, unique per customer within a namespace. Partial so it is enforced
129+
// only while live: NULL means no idempotency requested, and a soft-deleted grant must
130+
// not permanently reserve a key the caller may reuse.
131+
index.Fields("namespace", "customer_id", "key").
132132
Annotations(
133133
entsql.IndexWhere("key IS NOT NULL AND deleted_at IS NULL"),
134134
).
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- reverse: create index "chargecreditpurchase_namespace_customer_id_key" to table: "charge_credit_purchases"
2+
DROP INDEX "chargecreditpurchase_namespace_customer_id_key";
3+
-- reverse: drop index "chargecreditpurchase_namespace_key" from table: "charge_credit_purchases"
4+
CREATE UNIQUE INDEX "chargecreditpurchase_namespace_key" ON "charge_credit_purchases" ("namespace", "key") WHERE ((key IS NOT NULL) AND (deleted_at IS NULL));
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- drop index "chargecreditpurchase_namespace_key" from table: "charge_credit_purchases"
2+
DROP INDEX "chargecreditpurchase_namespace_key";
3+
-- create index "chargecreditpurchase_namespace_customer_id_key" to table: "charge_credit_purchases"
4+
-- the old (namespace, key) unique index guarantees no (namespace, customer_id, key) duplicates exist,
5+
-- so widening the key scope cannot fail on existing data
6+
-- atlas:nolint MF101
7+
CREATE UNIQUE INDEX "chargecreditpurchase_namespace_customer_id_key" ON "charge_credit_purchases" ("namespace", "customer_id", "key") WHERE ((key IS NOT NULL) AND (deleted_at IS NULL));

0 commit comments

Comments
 (0)