@porulle/plugin-giftcards 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/routes/admin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAG7D,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACtE,uBAAuB,EAAE,CAyG3B"}
1
+ {"version":3,"file":"admin.d.ts","sourceRoot":"","sources":["../../src/routes/admin.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AACxE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AAG7D,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,eAAe,EACxB,GAAG,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAAC,QAAQ,CAAC,EAAE;QAAE,EAAE,EAAE,OAAO,CAAA;KAAE,CAAA;CAAE,GACtE,uBAAuB,EAAE,CA0G3B"}
@@ -1,4 +1,4 @@
1
- import { router } from "@porulle/core";
1
+ import { router, CommerceNotFoundError } from "@porulle/core";
2
2
  import { z } from "@hono/zod-openapi";
3
3
  import { formatCode } from "../code-generator.js";
4
4
  export function buildAdminRoutes(service, ctx) {
@@ -52,8 +52,9 @@ export function buildAdminRoutes(service, ctx) {
52
52
  .permission("gift-cards:admin")
53
53
  .handler(async ({ params, orgId }) => {
54
54
  const cardResult = await service.getById(orgId, params.id);
55
+ // Cross-tenant / missing card → safe 404, not a generic 500.
55
56
  if (!cardResult.ok)
56
- throw new Error(cardResult.error);
57
+ throw new CommerceNotFoundError("Gift card not found.");
57
58
  const txnResult = await service.getTransactions(orgId, cardResult.value.id);
58
59
  return {
59
60
  ...cardResult.value,
@@ -68,7 +69,7 @@ export function buildAdminRoutes(service, ctx) {
68
69
  .handler(async ({ params, orgId }) => {
69
70
  const result = await service.disable(orgId, params.id);
70
71
  if (!result.ok)
71
- throw new Error(result.error);
72
+ throw new CommerceNotFoundError("Gift card not found.");
72
73
  return result.value;
73
74
  });
74
75
  // ─── Manual Balance Adjustment ────────────────────────────────────
@@ -83,7 +84,7 @@ export function buildAdminRoutes(service, ctx) {
83
84
  const body = input;
84
85
  const result = await service.adjust(orgId, params.id, body.delta, body.note);
85
86
  if (!result.ok)
86
- throw new Error(result.error);
87
+ throw new CommerceNotFoundError("Gift card not found.");
87
88
  return result.value;
88
89
  });
89
90
  return r.routes();
@@ -6,25 +6,25 @@ export declare class GiftCardRepository {
6
6
  create(data: GiftCardInsert, ctx?: {
7
7
  tx?: Db;
8
8
  }): Promise<GiftCard>;
9
- findById(id: string, ctx?: {
9
+ findById(orgId: string, id: string, ctx?: {
10
10
  tx?: Db;
11
11
  }): Promise<GiftCard | undefined>;
12
- findByCode(code: string, ctx?: {
12
+ findByCode(orgId: string, code: string, ctx?: {
13
13
  tx?: Db;
14
14
  }): Promise<GiftCard | undefined>;
15
- list(filters?: {
15
+ list(orgId: string, filters?: {
16
16
  status?: GiftCardStatus;
17
17
  purchaserId?: string;
18
18
  }, ctx?: {
19
19
  tx?: Db;
20
20
  }): Promise<GiftCard[]>;
21
- disable(id: string, ctx?: {
21
+ disable(orgId: string, id: string, ctx?: {
22
22
  tx?: Db;
23
23
  }): Promise<GiftCard | undefined>;
24
- findByCodeForUpdate(code: string, tx: Db): Promise<GiftCard | undefined>;
25
- findByIdForUpdate(id: string, tx: Db): Promise<GiftCard | undefined>;
26
- updateBalance(id: string, balance: number, status: GiftCardStatus, currentVersion: number, tx: Db): Promise<GiftCard>;
27
- adjustBalance(id: string, delta: number, tx: Db): Promise<GiftCard>;
24
+ findByCodeForUpdate(orgId: string, code: string, tx: Db): Promise<GiftCard | undefined>;
25
+ findByIdForUpdate(orgId: string, id: string, tx: Db): Promise<GiftCard | undefined>;
26
+ updateBalance(orgId: string, id: string, balance: number, status: GiftCardStatus, currentVersion: number, tx: Db): Promise<GiftCard>;
27
+ adjustBalance(orgId: string, id: string, delta: number, tx: Db): Promise<GiftCard>;
28
28
  recordTransaction(data: {
29
29
  giftCardId: string;
30
30
  type: TransactionType;
@@ -1 +1 @@
1
- {"version":3,"file":"gift-card-repository.d.ts","sourceRoot":"","sources":["../../src/services/gift-card-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,mBAAmB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtH,qBAAa,kBAAkB;IACjB,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAE1B,OAAO,CAAC,KAAK;IAMP,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQlE,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAQtE,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAQ1E,IAAI,CACR,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,cAAc,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAC3D,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAChB,OAAO,CAAC,QAAQ,EAAE,CAAC;IAYhB,OAAO,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAWrE,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IASxE,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IASpE,aAAa,CACjB,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,EACtB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,EAAE,GACL,OAAO,CAAC,QAAQ,CAAC;IAcd,aAAa,CACjB,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,EAAE,GACL,OAAO,CAAC,QAAQ,CAAC;IAYd,iBAAiB,CACrB,IAAI,EAAE;QACJ,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,eAAe,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,EACD,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAChB,OAAO,CAAC,mBAAmB,CAAC;IAQzB,gBAAgB,CACpB,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAChB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAQ3B,yBAAyB,CAC7B,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAChB,OAAO,CAAC,mBAAmB,EAAE,CAAC;CAOlC"}
1
+ {"version":3,"file":"gift-card-repository.d.ts","sourceRoot":"","sources":["../../src/services/gift-card-repository.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,mBAAmB,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAEtH,qBAAa,kBAAkB;IACjB,OAAO,CAAC,EAAE;gBAAF,EAAE,EAAE,EAAE;IAE1B,OAAO,CAAC,KAAK;IAMP,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC;IAQlE,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAQrF,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAYzF,IAAI,CACR,KAAK,EAAE,MAAM,EACb,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,cAAc,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,EAC3D,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAChB,OAAO,CAAC,QAAQ,EAAE,CAAC;IAYhB,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAWpF,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IASvF,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IASnF,aAAa,CACjB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,EACtB,cAAc,EAAE,MAAM,EACtB,EAAE,EAAE,EAAE,GACL,OAAO,CAAC,QAAQ,CAAC;IAcd,aAAa,CACjB,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,EAAE,GACL,OAAO,CAAC,QAAQ,CAAC;IAYd,iBAAiB,CACrB,IAAI,EAAE;QACJ,UAAU,EAAE,MAAM,CAAC;QACnB,IAAI,EAAE,eAAe,CAAC;QACtB,MAAM,EAAE,MAAM,CAAC;QACf,YAAY,EAAE,MAAM,CAAC;QACrB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,EACD,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAChB,OAAO,CAAC,mBAAmB,CAAC;IAQzB,gBAAgB,CACpB,UAAU,EAAE,MAAM,EAClB,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAChB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAQ3B,yBAAyB,CAC7B,OAAO,EAAE,MAAM,EACf,GAAG,CAAC,EAAE;QAAE,EAAE,CAAC,EAAE,EAAE,CAAA;KAAE,GAChB,OAAO,CAAC,mBAAmB,EAAE,CAAC;CAOlC"}
@@ -16,22 +16,27 @@ export class GiftCardRepository {
16
16
  .returning();
17
17
  return rows[0];
18
18
  }
19
- async findById(id, ctx) {
19
+ async findById(orgId, id, ctx) {
20
20
  const rows = await this.getDb(ctx)
21
21
  .select()
22
22
  .from(giftCards)
23
- .where(eq(giftCards.id, id));
23
+ .where(and(eq(giftCards.id, id), eq(giftCards.organizationId, orgId)));
24
24
  return rows[0];
25
25
  }
26
- async findByCode(code, ctx) {
26
+ async findByCode(orgId, code, ctx) {
27
+ const conditions = [eq(giftCards.code, code)];
28
+ // "_any" is the public bearer-token lookup (check-balance): a gift-card code
29
+ // is org-agnostic by design. Admin id-based lookups stay strictly org-scoped.
30
+ if (orgId !== "_any")
31
+ conditions.push(eq(giftCards.organizationId, orgId));
27
32
  const rows = await this.getDb(ctx)
28
33
  .select()
29
34
  .from(giftCards)
30
- .where(eq(giftCards.code, code));
35
+ .where(and(...conditions));
31
36
  return rows[0];
32
37
  }
33
- async list(filters, ctx) {
34
- const conditions = [];
38
+ async list(orgId, filters, ctx) {
39
+ const conditions = [eq(giftCards.organizationId, orgId)];
35
40
  if (filters?.status)
36
41
  conditions.push(eq(giftCards.status, filters.status));
37
42
  if (filters?.purchaserId)
@@ -39,35 +44,35 @@ export class GiftCardRepository {
39
44
  return this.getDb(ctx)
40
45
  .select()
41
46
  .from(giftCards)
42
- .where(conditions.length > 0 ? and(...conditions) : undefined)
47
+ .where(and(...conditions))
43
48
  .orderBy(desc(giftCards.createdAt));
44
49
  }
45
- async disable(id, ctx) {
50
+ async disable(orgId, id, ctx) {
46
51
  const rows = await this.getDb(ctx)
47
52
  .update(giftCards)
48
53
  .set({ status: "disabled", updatedAt: new Date() })
49
- .where(eq(giftCards.id, id))
54
+ .where(and(eq(giftCards.id, id), eq(giftCards.organizationId, orgId)))
50
55
  .returning();
51
56
  return rows[0];
52
57
  }
53
58
  // ─── SELECT FOR UPDATE (Concurrency-Safe Balance Operations) ───────
54
- async findByCodeForUpdate(code, tx) {
59
+ async findByCodeForUpdate(orgId, code, tx) {
55
60
  const rows = await tx
56
61
  .select()
57
62
  .from(giftCards)
58
- .where(eq(giftCards.code, code))
63
+ .where(and(eq(giftCards.code, code), eq(giftCards.organizationId, orgId)))
59
64
  .for("update");
60
65
  return rows[0];
61
66
  }
62
- async findByIdForUpdate(id, tx) {
67
+ async findByIdForUpdate(orgId, id, tx) {
63
68
  const rows = await tx
64
69
  .select()
65
70
  .from(giftCards)
66
- .where(eq(giftCards.id, id))
71
+ .where(and(eq(giftCards.id, id), eq(giftCards.organizationId, orgId)))
67
72
  .for("update");
68
73
  return rows[0];
69
74
  }
70
- async updateBalance(id, balance, status, currentVersion, tx) {
75
+ async updateBalance(orgId, id, balance, status, currentVersion, tx) {
71
76
  const rows = await tx
72
77
  .update(giftCards)
73
78
  .set({
@@ -76,17 +81,17 @@ export class GiftCardRepository {
76
81
  version: currentVersion + 1,
77
82
  updatedAt: new Date(),
78
83
  })
79
- .where(eq(giftCards.id, id))
84
+ .where(and(eq(giftCards.id, id), eq(giftCards.organizationId, orgId)))
80
85
  .returning();
81
86
  return rows[0];
82
87
  }
83
- async adjustBalance(id, delta, tx) {
84
- const card = await this.findByIdForUpdate(id, tx);
88
+ async adjustBalance(orgId, id, delta, tx) {
89
+ const card = await this.findByIdForUpdate(orgId, id, tx);
85
90
  if (!card)
86
91
  throw new Error("Gift card not found");
87
92
  const newBalance = Math.max(0, Math.min(card.initialAmount, card.balance + delta));
88
93
  const newStatus = newBalance === 0 ? "exhausted" : "active";
89
- return this.updateBalance(id, newBalance, newStatus, card.version, tx);
94
+ return this.updateBalance(orgId, id, newBalance, newStatus, card.version, tx);
90
95
  }
91
96
  // ─── Transactions ───────────────────────────────────────────────────
92
97
  async recordTransaction(data, ctx) {
@@ -1 +1 @@
1
- {"version":3,"file":"gift-card-service.d.ts","sourceRoot":"","sources":["../../src/services/gift-card-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,KAAK,EACV,EAAE,EACF,QAAQ,EACR,iBAAiB,EACjB,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EAEf,MAAM,aAAa,CAAC;AAErB,qBAAa,eAAe;IAOxB,OAAO,CAAC,OAAO;IANjB,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,WAAW,CAAyD;gBAG1E,EAAE,EAAE,EAAE,EACN,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,EAC7D,OAAO,EAAE,QAAQ,CAAC,qBAAqB,CAAC;IAQ5C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAuD7B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAMnE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAMvE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAClC,MAAM,CAAC,EAAE,cAAc,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC;IAK/B,eAAe,CACnB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,CAAC,mBAAmB,EAAE,CAAC,CAAC;IAOzC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QACpE,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;IAaH;;;OAGG;IACG,aAAa,CACjB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;IAwC3C;;;OAGG;IACG,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAsC5C,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAMnE,MAAM,CACV,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;CAoCnC"}
1
+ {"version":3,"file":"gift-card-service.d.ts","sourceRoot":"","sources":["../../src/services/gift-card-service.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,KAAK,EACV,EAAE,EACF,QAAQ,EACR,iBAAiB,EACjB,qBAAqB,EACrB,mBAAmB,EACnB,cAAc,EAEf,MAAM,aAAa,CAAC;AAErB,qBAAa,eAAe;IAOxB,OAAO,CAAC,OAAO;IANjB,OAAO,CAAC,IAAI,CAAqB;IACjC,OAAO,CAAC,WAAW,CAAyD;gBAG1E,EAAE,EAAE,EAAE,EACN,aAAa,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,EAC7D,OAAO,EAAE,QAAQ,CAAC,qBAAqB,CAAC;IAQ5C,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE;QACjC,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,cAAc,CAAC,EAAE,MAAM,CAAC;QACxB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,eAAe,CAAC,EAAE,MAAM,CAAC;QACzB,aAAa,CAAC,EAAE,MAAM,CAAC;QACvB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;KACpC,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAuD7B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAMnE,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAMvE,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAClC,MAAM,CAAC,EAAE,cAAc,CAAC;QACxB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,EAAE,CAAC,CAAC;IAK/B,eAAe,CACnB,KAAK,EAAE,MAAM,EACb,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,YAAY,CAAC,mBAAmB,EAAE,CAAC,CAAC;IAOzC,YAAY,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;QACpE,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC,CAAC;IAaH;;;OAGG;IACG,aAAa,CACjB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,YAAY,CAAC,iBAAiB,CAAC,CAAC;IAwC3C;;;OAGG;IACG,cAAc,CAClB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAsC5C,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;IAMnE,MAAM,CACV,KAAK,EAAE,MAAM,EACb,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;CAqCnC"}
@@ -23,7 +23,7 @@ export class GiftCardService {
23
23
  let attempts = 0;
24
24
  do {
25
25
  code = normalizeCode(generateGiftCardCode(this.options.codeFormat));
26
- const existing = await this.repo.findByCode(code);
26
+ const existing = await this.repo.findByCode(orgId, code);
27
27
  if (!existing)
28
28
  break;
29
29
  attempts++;
@@ -60,19 +60,19 @@ export class GiftCardService {
60
60
  }
61
61
  // ─── Query ───────────────────────────────────────────────────────────
62
62
  async getById(orgId, id) {
63
- const card = await this.repo.findById(id);
63
+ const card = await this.repo.findById(orgId, id);
64
64
  if (!card)
65
65
  return Err("Gift card not found");
66
66
  return Ok(card);
67
67
  }
68
68
  async getByCode(orgId, code) {
69
- const card = await this.repo.findByCode(normalizeCode(code));
69
+ const card = await this.repo.findByCode(orgId, normalizeCode(code));
70
70
  if (!card)
71
71
  return Err("Gift card not found");
72
72
  return Ok(card);
73
73
  }
74
74
  async list(orgId, filters) {
75
- const cards = await this.repo.list(filters);
75
+ const cards = await this.repo.list(orgId, filters);
76
76
  return Ok(cards);
77
77
  }
78
78
  async getTransactions(orgId, giftCardId) {
@@ -81,7 +81,7 @@ export class GiftCardService {
81
81
  }
82
82
  // ─── Balance Check (Public) ──────────────────────────────────────────
83
83
  async checkBalance(orgId, code) {
84
- const card = await this.repo.findByCode(normalizeCode(code));
84
+ const card = await this.repo.findByCode(orgId, normalizeCode(code));
85
85
  if (!card)
86
86
  return Err("Gift card not found");
87
87
  return Ok({
@@ -99,7 +99,7 @@ export class GiftCardService {
99
99
  if (amount <= 0)
100
100
  return Err("Debit amount must be positive");
101
101
  const result = await this.transaction(async (tx) => {
102
- const card = await this.repo.findByCodeForUpdate(normalizeCode(code), tx);
102
+ const card = await this.repo.findByCodeForUpdate(orgId, normalizeCode(code), tx);
103
103
  if (!card)
104
104
  return Err("GIFT_CARD_NOT_FOUND");
105
105
  if (card.status === "disabled")
@@ -114,7 +114,7 @@ export class GiftCardService {
114
114
  return Err("INSUFFICIENT_BALANCE");
115
115
  const balanceAfter = card.balance - amount;
116
116
  const newStatus = balanceAfter === 0 ? "exhausted" : "active";
117
- await this.repo.updateBalance(card.id, balanceAfter, newStatus, card.version, tx);
117
+ await this.repo.updateBalance(orgId, card.id, balanceAfter, newStatus, card.version, tx);
118
118
  await this.repo.recordTransaction({
119
119
  giftCardId: card.id,
120
120
  type: "debit",
@@ -140,7 +140,7 @@ export class GiftCardService {
140
140
  if (amount <= 0)
141
141
  return Err("Credit amount must be positive");
142
142
  const result = await this.transaction(async (tx) => {
143
- const card = await this.repo.findByCodeForUpdate(normalizeCode(code), tx);
143
+ const card = await this.repo.findByCodeForUpdate(orgId, normalizeCode(code), tx);
144
144
  if (!card)
145
145
  return Err("GIFT_CARD_NOT_FOUND");
146
146
  // Cap credit at initial amount (prevent inflation attack)
@@ -150,7 +150,7 @@ export class GiftCardService {
150
150
  return Ok({ balanceAfter: card.balance });
151
151
  }
152
152
  const newStatus = balanceAfter > 0 ? "active" : card.status;
153
- await this.repo.updateBalance(card.id, balanceAfter, newStatus, card.version, tx);
153
+ await this.repo.updateBalance(orgId, card.id, balanceAfter, newStatus, card.version, tx);
154
154
  await this.repo.recordTransaction({
155
155
  giftCardId: card.id,
156
156
  type: "refund",
@@ -165,20 +165,20 @@ export class GiftCardService {
165
165
  }
166
166
  // ─── Admin Operations ────────────────────────────────────────────────
167
167
  async disable(orgId, id) {
168
- const card = await this.repo.disable(id);
168
+ const card = await this.repo.disable(orgId, id);
169
169
  if (!card)
170
170
  return Err("Gift card not found");
171
171
  return Ok(card);
172
172
  }
173
173
  async adjust(orgId, id, delta, note) {
174
174
  const result = await this.transaction(async (tx) => {
175
- const card = await this.repo.findByIdForUpdate(id, tx);
175
+ const card = await this.repo.findByIdForUpdate(orgId, id, tx);
176
176
  if (!card)
177
177
  return Err("Gift card not found");
178
178
  const newBalance = Math.max(0, Math.min(card.initialAmount, card.balance + delta));
179
179
  const actualDelta = newBalance - card.balance;
180
180
  const newStatus = newBalance === 0 ? "exhausted" : "active";
181
- const updated = await this.repo.updateBalance(card.id, newBalance, newStatus, card.version, tx);
181
+ const updated = await this.repo.updateBalance(orgId, card.id, newBalance, newStatus, card.version, tx);
182
182
  if (actualDelta !== 0) {
183
183
  const txnType = actualDelta > 0 ? "credit" : "debit";
184
184
  await this.repo.recordTransaction({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@porulle/plugin-giftcards",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -19,7 +19,7 @@
19
19
  "dependencies": {
20
20
  "@hono/zod-openapi": "^1.2.2",
21
21
  "hono": "^4.12.5",
22
- "@porulle/core": "0.8.0"
22
+ "@porulle/core": "0.9.0"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@types/node": "^24.5.2",
@@ -1,4 +1,4 @@
1
- import { router } from "@porulle/core";
1
+ import { router, CommerceNotFoundError } from "@porulle/core";
2
2
  import { z } from "@hono/zod-openapi";
3
3
  import type { GiftCardService } from "../services/gift-card-service.js";
4
4
  import type { PluginRouteRegistration } from "@porulle/core";
@@ -72,7 +72,8 @@ export function buildAdminRoutes(
72
72
  .permission("gift-cards:admin")
73
73
  .handler(async ({ params, orgId }) => {
74
74
  const cardResult = await service.getById(orgId, params.id!);
75
- if (!cardResult.ok) throw new Error(cardResult.error);
75
+ // Cross-tenant / missing card → safe 404, not a generic 500.
76
+ if (!cardResult.ok) throw new CommerceNotFoundError("Gift card not found.");
76
77
 
77
78
  const txnResult = await service.getTransactions(orgId, cardResult.value.id);
78
79
  return {
@@ -89,7 +90,7 @@ export function buildAdminRoutes(
89
90
  .permission("gift-cards:admin")
90
91
  .handler(async ({ params, orgId }) => {
91
92
  const result = await service.disable(orgId, params.id!);
92
- if (!result.ok) throw new Error(result.error);
93
+ if (!result.ok) throw new CommerceNotFoundError("Gift card not found.");
93
94
  return result.value;
94
95
  });
95
96
 
@@ -107,7 +108,7 @@ export function buildAdminRoutes(
107
108
  .handler(async ({ params, input, orgId }) => {
108
109
  const body = input as { delta: number; note: string };
109
110
  const result = await service.adjust(orgId, params.id!, body.delta, body.note);
110
- if (!result.ok) throw new Error(result.error);
111
+ if (!result.ok) throw new CommerceNotFoundError("Gift card not found.");
111
112
  return result.value;
112
113
  });
113
114
 
@@ -19,67 +19,73 @@ export class GiftCardRepository {
19
19
  return rows[0]!;
20
20
  }
21
21
 
22
- async findById(id: string, ctx?: { tx?: Db }): Promise<GiftCard | undefined> {
22
+ async findById(orgId: string, id: string, ctx?: { tx?: Db }): Promise<GiftCard | undefined> {
23
23
  const rows = await this.getDb(ctx)
24
24
  .select()
25
25
  .from(giftCards)
26
- .where(eq(giftCards.id, id));
26
+ .where(and(eq(giftCards.id, id), eq(giftCards.organizationId, orgId)));
27
27
  return rows[0];
28
28
  }
29
29
 
30
- async findByCode(code: string, ctx?: { tx?: Db }): Promise<GiftCard | undefined> {
30
+ async findByCode(orgId: string, code: string, ctx?: { tx?: Db }): Promise<GiftCard | undefined> {
31
+ const conditions = [eq(giftCards.code, code)];
32
+ // "_any" is the public bearer-token lookup (check-balance): a gift-card code
33
+ // is org-agnostic by design. Admin id-based lookups stay strictly org-scoped.
34
+ if (orgId !== "_any") conditions.push(eq(giftCards.organizationId, orgId));
31
35
  const rows = await this.getDb(ctx)
32
36
  .select()
33
37
  .from(giftCards)
34
- .where(eq(giftCards.code, code));
38
+ .where(and(...conditions));
35
39
  return rows[0];
36
40
  }
37
41
 
38
42
  async list(
43
+ orgId: string,
39
44
  filters?: { status?: GiftCardStatus; purchaserId?: string },
40
45
  ctx?: { tx?: Db },
41
46
  ): Promise<GiftCard[]> {
42
- const conditions = [];
47
+ const conditions = [eq(giftCards.organizationId, orgId)];
43
48
  if (filters?.status) conditions.push(eq(giftCards.status, filters.status));
44
49
  if (filters?.purchaserId) conditions.push(eq(giftCards.purchaserId, filters.purchaserId));
45
50
 
46
51
  return this.getDb(ctx)
47
52
  .select()
48
53
  .from(giftCards)
49
- .where(conditions.length > 0 ? and(...conditions) : undefined)
54
+ .where(and(...conditions))
50
55
  .orderBy(desc(giftCards.createdAt));
51
56
  }
52
57
 
53
- async disable(id: string, ctx?: { tx?: Db }): Promise<GiftCard | undefined> {
58
+ async disable(orgId: string, id: string, ctx?: { tx?: Db }): Promise<GiftCard | undefined> {
54
59
  const rows = await this.getDb(ctx)
55
60
  .update(giftCards)
56
61
  .set({ status: "disabled" as const, updatedAt: new Date() })
57
- .where(eq(giftCards.id, id))
62
+ .where(and(eq(giftCards.id, id), eq(giftCards.organizationId, orgId)))
58
63
  .returning();
59
64
  return rows[0];
60
65
  }
61
66
 
62
67
  // ─── SELECT FOR UPDATE (Concurrency-Safe Balance Operations) ───────
63
68
 
64
- async findByCodeForUpdate(code: string, tx: Db): Promise<GiftCard | undefined> {
69
+ async findByCodeForUpdate(orgId: string, code: string, tx: Db): Promise<GiftCard | undefined> {
65
70
  const rows = await tx
66
71
  .select()
67
72
  .from(giftCards)
68
- .where(eq(giftCards.code, code))
73
+ .where(and(eq(giftCards.code, code), eq(giftCards.organizationId, orgId)))
69
74
  .for("update");
70
75
  return rows[0];
71
76
  }
72
77
 
73
- async findByIdForUpdate(id: string, tx: Db): Promise<GiftCard | undefined> {
78
+ async findByIdForUpdate(orgId: string, id: string, tx: Db): Promise<GiftCard | undefined> {
74
79
  const rows = await tx
75
80
  .select()
76
81
  .from(giftCards)
77
- .where(eq(giftCards.id, id))
82
+ .where(and(eq(giftCards.id, id), eq(giftCards.organizationId, orgId)))
78
83
  .for("update");
79
84
  return rows[0];
80
85
  }
81
86
 
82
87
  async updateBalance(
88
+ orgId: string,
83
89
  id: string,
84
90
  balance: number,
85
91
  status: GiftCardStatus,
@@ -94,23 +100,24 @@ export class GiftCardRepository {
94
100
  version: currentVersion + 1,
95
101
  updatedAt: new Date(),
96
102
  })
97
- .where(eq(giftCards.id, id))
103
+ .where(and(eq(giftCards.id, id), eq(giftCards.organizationId, orgId)))
98
104
  .returning();
99
105
  return rows[0]!;
100
106
  }
101
107
 
102
108
  async adjustBalance(
109
+ orgId: string,
103
110
  id: string,
104
111
  delta: number,
105
112
  tx: Db,
106
113
  ): Promise<GiftCard> {
107
- const card = await this.findByIdForUpdate(id, tx);
114
+ const card = await this.findByIdForUpdate(orgId, id, tx);
108
115
  if (!card) throw new Error("Gift card not found");
109
116
 
110
117
  const newBalance = Math.max(0, Math.min(card.initialAmount, card.balance + delta));
111
118
  const newStatus: GiftCardStatus = newBalance === 0 ? "exhausted" : "active";
112
119
 
113
- return this.updateBalance(id, newBalance, newStatus, card.version, tx);
120
+ return this.updateBalance(orgId, id, newBalance, newStatus, card.version, tx);
114
121
  }
115
122
 
116
123
  // ─── Transactions ───────────────────────────────────────────────────
@@ -49,7 +49,7 @@ export class GiftCardService {
49
49
  let attempts = 0;
50
50
  do {
51
51
  code = normalizeCode(generateGiftCardCode(this.options.codeFormat));
52
- const existing = await this.repo.findByCode(code);
52
+ const existing = await this.repo.findByCode(orgId, code);
53
53
  if (!existing) break;
54
54
  attempts++;
55
55
  } while (attempts < 10);
@@ -92,13 +92,13 @@ export class GiftCardService {
92
92
  // ─── Query ───────────────────────────────────────────────────────────
93
93
 
94
94
  async getById(orgId: string, id: string): Promise<PluginResult<GiftCard>> {
95
- const card = await this.repo.findById(id);
95
+ const card = await this.repo.findById(orgId, id);
96
96
  if (!card) return Err("Gift card not found");
97
97
  return Ok(card);
98
98
  }
99
99
 
100
100
  async getByCode(orgId: string, code: string): Promise<PluginResult<GiftCard>> {
101
- const card = await this.repo.findByCode(normalizeCode(code));
101
+ const card = await this.repo.findByCode(orgId, normalizeCode(code));
102
102
  if (!card) return Err("Gift card not found");
103
103
  return Ok(card);
104
104
  }
@@ -107,7 +107,7 @@ export class GiftCardService {
107
107
  status?: GiftCardStatus;
108
108
  purchaserId?: string;
109
109
  }): Promise<PluginResult<GiftCard[]>> {
110
- const cards = await this.repo.list(filters);
110
+ const cards = await this.repo.list(orgId, filters);
111
111
  return Ok(cards);
112
112
  }
113
113
 
@@ -126,7 +126,7 @@ export class GiftCardService {
126
126
  currency: string;
127
127
  status: string;
128
128
  }>> {
129
- const card = await this.repo.findByCode(normalizeCode(code));
129
+ const card = await this.repo.findByCode(orgId, normalizeCode(code));
130
130
  if (!card) return Err("Gift card not found");
131
131
 
132
132
  return Ok({
@@ -152,7 +152,7 @@ export class GiftCardService {
152
152
  if (amount <= 0) return Err("Debit amount must be positive");
153
153
 
154
154
  const result = await this.transaction(async (tx) => {
155
- const card = await this.repo.findByCodeForUpdate(normalizeCode(code), tx);
155
+ const card = await this.repo.findByCodeForUpdate(orgId, normalizeCode(code), tx);
156
156
  if (!card) return Err("GIFT_CARD_NOT_FOUND");
157
157
  if (card.status === "disabled") return Err("GIFT_CARD_INACTIVE");
158
158
  if (card.status === "exhausted") return Err("GIFT_CARD_EXHAUSTED");
@@ -163,7 +163,7 @@ export class GiftCardService {
163
163
  const balanceAfter = card.balance - amount;
164
164
  const newStatus: GiftCardStatus = balanceAfter === 0 ? "exhausted" : "active";
165
165
 
166
- await this.repo.updateBalance(card.id, balanceAfter, newStatus, card.version, tx);
166
+ await this.repo.updateBalance(orgId, card.id, balanceAfter, newStatus, card.version, tx);
167
167
  await this.repo.recordTransaction(
168
168
  {
169
169
  giftCardId: card.id,
@@ -202,7 +202,7 @@ export class GiftCardService {
202
202
  if (amount <= 0) return Err("Credit amount must be positive");
203
203
 
204
204
  const result = await this.transaction(async (tx) => {
205
- const card = await this.repo.findByCodeForUpdate(normalizeCode(code), tx);
205
+ const card = await this.repo.findByCodeForUpdate(orgId, normalizeCode(code), tx);
206
206
  if (!card) return Err("GIFT_CARD_NOT_FOUND");
207
207
 
208
208
  // Cap credit at initial amount (prevent inflation attack)
@@ -215,7 +215,7 @@ export class GiftCardService {
215
215
 
216
216
  const newStatus: GiftCardStatus = balanceAfter > 0 ? "active" : card.status;
217
217
 
218
- await this.repo.updateBalance(card.id, balanceAfter, newStatus, card.version, tx);
218
+ await this.repo.updateBalance(orgId, card.id, balanceAfter, newStatus, card.version, tx);
219
219
  await this.repo.recordTransaction(
220
220
  {
221
221
  giftCardId: card.id,
@@ -237,7 +237,7 @@ export class GiftCardService {
237
237
  // ─── Admin Operations ────────────────────────────────────────────────
238
238
 
239
239
  async disable(orgId: string, id: string): Promise<PluginResult<GiftCard>> {
240
- const card = await this.repo.disable(id);
240
+ const card = await this.repo.disable(orgId, id);
241
241
  if (!card) return Err("Gift card not found");
242
242
  return Ok(card);
243
243
  }
@@ -249,7 +249,7 @@ export class GiftCardService {
249
249
  note: string,
250
250
  ): Promise<PluginResult<GiftCard>> {
251
251
  const result = await this.transaction(async (tx) => {
252
- const card = await this.repo.findByIdForUpdate(id, tx);
252
+ const card = await this.repo.findByIdForUpdate(orgId, id, tx);
253
253
  if (!card) return Err("Gift card not found");
254
254
 
255
255
  const newBalance = Math.max(0, Math.min(card.initialAmount, card.balance + delta));
@@ -257,6 +257,7 @@ export class GiftCardService {
257
257
  const newStatus: GiftCardStatus = newBalance === 0 ? "exhausted" : "active";
258
258
 
259
259
  const updated = await this.repo.updateBalance(
260
+ orgId,
260
261
  card.id,
261
262
  newBalance,
262
263
  newStatus,