diff --git a/e2e/cloud-store.spec.ts b/e2e/cloud-store.spec.ts index 52c0e012..e6553e4a 100644 --- a/e2e/cloud-store.spec.ts +++ b/e2e/cloud-store.spec.ts @@ -24,11 +24,6 @@ type CloudGiftCard = { code: string } -type CloudGiftCardPage = { - items: CloudGiftCard[] - total: number -} - type CloudOrder = { id: string paymentStatus: string @@ -226,13 +221,13 @@ async function createOneTimePackage(page: Page, name: string) { } async function createGiftCard(page: Page) { - const cards = await postJson(page, '/api/admin/store/gift-cards', { + const cards = await postJson(page, '/api/admin/store/gift-cards', { amount: 200, currency: 'usd', count: 1, }) - expect(cards.items.length).toBe(1) - return cards.items[0] + expect(cards.length).toBe(1) + return cards[0] } async function createOneTimePackageThroughUi(page: Page, packageName: string) { @@ -271,9 +266,9 @@ async function createGiftCardThroughUi(page: Page) { await dialog.getByRole('button', { name: 'Generate' }).click() const result = await response expect(result.status()).toBe(201) - const cards = (await result.json()) as CloudGiftCardPage - expect(cards.items.length).toBe(1) - return cards.items[0].code + const cards = (await result.json()) as CloudGiftCard[] + expect(cards.length).toBe(1) + return cards[0].code } async function expectAdminProductVisibleInApi(page: Page, packageName: string) { diff --git a/server/routes/cloud-store-helpers.test.ts b/server/routes/cloud-store-helpers.test.ts index baac731d..2bff6224 100644 --- a/server/routes/cloud-store-helpers.test.ts +++ b/server/routes/cloud-store-helpers.test.ts @@ -140,15 +140,14 @@ describe('quota store helper paths', () => { items: [ { id: 'gift-1', - boundLicenseId: null, - code: 'ZS-PAGED-1', + storeId: 'store-1', + campaignId: null, + code: null, + codeLast4: 'GED1', amount: 2500, currency: 'usd', status: 'active', expiresAt: null, - firstRedeemedAt: '2026-05-07T00:00:30.000Z', - lastRedeemedAt: '2026-05-07T00:01:00.000Z', - redemptionCount: 1, createdAt: '2026-05-07T00:00:00.000Z', updatedAt: '2026-05-07T00:01:00.000Z', disabledAt: null, @@ -162,15 +161,14 @@ describe('quota store helper paths', () => { items: [ { id: 'gift-1', - boundLicenseId: null, - code: 'ZS-PAGED-1', + storeId: 'store-1', + campaignId: null, + code: null, + codeLast4: 'GED1', amount: 2500, currency: 'usd', status: 'active', expiresAt: null, - firstRedeemedAt: '2026-05-07T00:00:30.000Z', - lastRedeemedAt: '2026-05-07T00:01:00.000Z', - redemptionCount: 1, createdAt: '2026-05-07T00:00:00.000Z', updatedAt: '2026-05-07T00:01:00.000Z', disabledAt: null, diff --git a/server/routes/cloud-store-helpers.ts b/server/routes/cloud-store-helpers.ts index f8d72d7c..34912943 100644 --- a/server/routes/cloud-store-helpers.ts +++ b/server/routes/cloud-store-helpers.ts @@ -169,16 +169,14 @@ export const cloudStoreOrdersQuerySchema = cloudOrdersQuerySchema export const cloudGiftCardSchema = z.object({ id: z.string().min(1), - storeId: z.string().min(1).nullable().optional(), - boundLicenseId: z.string().min(1).nullable(), - code: z.string().min(1), + storeId: z.string().min(1), + campaignId: z.string().min(1).nullable(), + code: z.string().min(1).nullable(), + codeLast4: z.string().min(1), amount: z.number().int().min(0), currency: z.string().min(1), - status: z.enum(['created', 'active', 'disabled', 'exhausted', 'expired', 'revoked']), + status: z.enum(['active', 'redeemed', 'disabled', 'expired', 'revoked']), expiresAt: z.string().nullable(), - firstRedeemedAt: z.string().nullable(), - lastRedeemedAt: z.string().nullable(), - redemptionCount: z.number().int().min(0), createdAt: z.string().min(1), updatedAt: z.string().min(1), disabledAt: z.string().nullable(), @@ -190,6 +188,7 @@ export const cloudGiftCardsResponseSchema = z.object({ items: z.array(cloudGiftCardSchema), total: z.number().int().min(0), }) +export const cloudGiftCardListSchema = z.array(cloudGiftCardSchema) export const giftCardListQuerySchema = z.object({ status: giftCardStatusSchema.optional() }) export async function getUserStoreSettings(db: Parameters[0]) { diff --git a/server/routes/cloud-store.integration.test.ts b/server/routes/cloud-store.integration.test.ts index d53aca1d..f4752c96 100644 --- a/server/routes/cloud-store.integration.test.ts +++ b/server/routes/cloud-store.integration.test.ts @@ -18,15 +18,14 @@ const { secretKey: EVENT_SECRET, publicKey: EVENT_PUBLIC } = generateKeys('publi const zpanCloudGiftCardResponseFixture: CloudGiftCard = { id: 'gift-card-1', - boundLicenseId: null, - code: 'ZS11-ACTV-0000-0001', + storeId: 'store-test-binding', + campaignId: null, + code: null, + codeLast4: '0001', amount: 1000, currency: 'usd', status: 'active', expiresAt: null, - firstRedeemedAt: null, - lastRedeemedAt: null, - redemptionCount: 0, createdAt: '2026-05-06T00:00:00.000Z', updatedAt: '2026-05-06T00:00:00.000Z', disabledAt: null, @@ -205,12 +204,12 @@ beforeEach(() => { ok: true, status: 200, json: async () => ({ - items: [cloudGiftCard({ code: 'ZS-LIST-1', amount: 1024, redemptionCount: 1 })], + items: [cloudGiftCard({ code: 'ZS-LIST-1', codeLast4: 'ST-1', amount: 1024 })], total: 1, limit: 50, offset: 0, data: { - items: [cloudGiftCard({ code: 'ZS-LIST-1', amount: 1024, redemptionCount: 1 })], + items: [cloudGiftCard({ code: 'ZS-LIST-1', codeLast4: 'ST-1', amount: 1024 })], total: 1, limit: 50, offset: 0, @@ -223,16 +222,14 @@ beforeEach(() => { ok: true, status: 201, json: async () => ({ - data: { - items: Array.from({ length: body.count ?? 1 }, (_, index) => - cloudGiftCard({ - code: `ZS-GEN-${index + 1}`, - amount: body.amount ?? 1024, - status: 'created', - }), - ), - total: body.count ?? 1, - }, + data: Array.from({ length: body.count ?? 1 }, (_, index) => + cloudGiftCard({ + code: `ZS-GEN-${index + 1}`, + codeLast4: `GEN${index + 1}`, + amount: body.amount ?? 1024, + status: 'active', + }), + ), }), } as Response } @@ -418,8 +415,8 @@ describe('Quota Store API', () => { items: [ cloudGiftCard({ code: 'ZS11-ACTV-0000-0001', + codeLast4: '0001', amount: 2048, - redemptionCount: 1, currency: 'usd', }), ], @@ -429,15 +426,14 @@ describe('Quota Store API', () => { items: [ { id: 'gift-card-1', - boundLicenseId: null, + storeId: 'store-test-binding', + campaignId: null, code: 'ZS11-ACTV-0000-0001', + codeLast4: '0001', amount: 2048, currency: 'usd', status: 'active', expiresAt: null, - firstRedeemedAt: null, - lastRedeemedAt: null, - redemptionCount: 1, createdAt: '2026-05-06T00:00:00.000Z', updatedAt: '2026-05-06T00:00:00.000Z', disabledAt: null, @@ -1304,17 +1300,14 @@ describe('Quota Store API', () => { const deleted = await app.request('/api/admin/store/gift-cards/ZS-GEN-1', { method: 'DELETE', headers }) expect(generated.status).toBe(201) - await expect(generated.json()).resolves.toMatchObject({ - total: 2, - items: [ - { code: 'ZS-GEN-1', amount: 4096, status: 'created' }, - { code: 'ZS-GEN-2', amount: 4096, status: 'created' }, - ], - }) + await expect(generated.json()).resolves.toMatchObject([ + { code: 'ZS-GEN-1', amount: 4096, status: 'active' }, + { code: 'ZS-GEN-2', amount: 4096, status: 'active' }, + ]) expect(listed.status).toBe(200) await expect(listed.json()).resolves.toMatchObject({ total: 1, - items: [{ code: 'ZS-LIST-1', amount: 1024, redemptionCount: 1 }], + items: [{ code: 'ZS-LIST-1', amount: 1024 }], }) expect(deleted.status).toBe(200) await expect(deleted.json()).resolves.toEqual({ code: 'ZS-GEN-1', deleted: true }) @@ -1354,7 +1347,7 @@ describe('Quota Store API', () => { await expect(res.json()).resolves.toMatchObject({ total: 9, items: [{ code: 'ZS-PAGED-1' }] }) }) - it('returns paged admin gift card create responses from Cloud', async () => { + it('returns admin gift card create responses from Cloud', async () => { const { app, db } = await createTestApp() await seedProLicense(db) const headers = await adminHeaders(app) @@ -1362,7 +1355,7 @@ describe('Quota Store API', () => { vi.mocked(fetch).mockResolvedValueOnce({ ok: true, status: 201, - json: async () => ({ items: [cloudGiftCard({ code: 'ZS-PAGED-1' })], total: 1 }), + json: async () => [cloudGiftCard({ code: 'ZS-CREATED-1', codeLast4: 'TED1' })], } as Response) const res = await app.request('/api/admin/store/gift-cards', { @@ -1376,7 +1369,7 @@ describe('Quota Store API', () => { }) expect(res.status).toBe(201) - await expect(res.json()).resolves.toMatchObject({ total: 1, items: [{ code: 'ZS-PAGED-1' }] }) + await expect(res.json()).resolves.toMatchObject([{ code: 'ZS-CREATED-1' }]) }) it('disables admin gift cards through Cloud', async () => { diff --git a/server/routes/cloud-store/admin.ts b/server/routes/cloud-store/admin.ts index baa6d539..b9dbfd58 100644 --- a/server/routes/cloud-store/admin.ts +++ b/server/routes/cloud-store/admin.ts @@ -13,6 +13,7 @@ import type { Env } from '../../middleware/platform' import { requireFeature } from '../../middleware/require-feature' import { getCloudStoreSettings, upsertCloudStoreSettings } from '../../services/cloud-store' import { + cloudGiftCardListSchema, cloudGiftCardsResponseSchema, cloudOrdersQuerySchema, cloudPackageListResponseSchema, @@ -152,7 +153,7 @@ export const adminCloudStore = new Hono() return c.json(result) }) .post('/gift-cards', zValidator('json', createGiftCardInputSchema), async (c) => { - const result = await postCloudWithBinding(c, giftCardsPath(), c.req.valid('json'), cloudGiftCardsResponseSchema) + const result = await postCloudWithBinding(c, giftCardsPath(), c.req.valid('json'), cloudGiftCardListSchema) if ('error' in result) return c.json(result, 502) return c.json(result, 201) }) diff --git a/shared/schemas/cloud-store.ts b/shared/schemas/cloud-store.ts index be4a498e..7da98b24 100644 --- a/shared/schemas/cloud-store.ts +++ b/shared/schemas/cloud-store.ts @@ -160,7 +160,7 @@ export const checkoutInputSchema = z.object({ priceId: z.string().min(1).optional(), }) -export const giftCardStatusSchema = z.enum(['created', 'active', 'disabled', 'exhausted', 'expired', 'revoked']) +export const giftCardStatusSchema = z.enum(['active', 'redeemed', 'disabled', 'expired', 'revoked']) export const createGiftCardInputSchema = z.object({ amount: z.number().int().positive(), diff --git a/shared/types/index.ts b/shared/types/index.ts index ce2f5314..0398d5b5 100644 --- a/shared/types/index.ts +++ b/shared/types/index.ts @@ -164,16 +164,14 @@ export interface CloudOrder { export interface CloudGiftCard { id: string - storeId?: string | null - boundLicenseId: string | null - code: string + storeId: string + campaignId: string | null + code: string | null + codeLast4: string amount: number currency: string - status: 'created' | 'active' | 'disabled' | 'exhausted' | 'expired' | 'revoked' + status: 'active' | 'redeemed' | 'disabled' | 'expired' | 'revoked' expiresAt: string | null - firstRedeemedAt: string | null - lastRedeemedAt: string | null - redemptionCount: number disabledAt: string | null revokedAt: string | null createdAt: string diff --git a/src/components/admin/cloud-gift-card-panel.tsx b/src/components/admin/cloud-gift-card-panel.tsx index 6aef4fc5..c6976270 100644 --- a/src/components/admin/cloud-gift-card-panel.tsx +++ b/src/components/admin/cloud-gift-card-panel.tsx @@ -196,7 +196,7 @@ function CodeStatusSelect({ status, onStatusChange }: Pick - {(['all', 'created', 'active', 'disabled', 'exhausted', 'expired', 'revoked'] as const).map((value) => ( + {(['all', 'active', 'redeemed', 'disabled', 'expired', 'revoked'] as const).map((value) => ( {t(`admin.cloudStore.codes.status.${value}`)} @@ -213,7 +213,7 @@ function CodeTable(props: CodeListProps) { - {['code', 'storage', 'uses', 'expires', 'statusLabel'].map((key) => ( + {['code', 'storage', 'expires', 'statusLabel'].map((key) => ( {t(`admin.cloudStore.codes.${key}`)} ))} {t('common.actions')} @@ -221,7 +221,7 @@ function CodeTable(props: CodeListProps) { {props.codes.map((code) => ( - + ))}
@@ -240,13 +240,14 @@ function CodeRow({ const { t } = useTranslation() const [disableConfirmOpen, setRevokeConfirmOpen] = useState(false) const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) - const canDisable = code.status === 'created' || code.status === 'active' - const canDelete = code.status === 'created' + const actionTarget = code.code ?? code.id + const codeLabel = code.code ?? code.codeLast4 + const canDisable = code.status === 'active' + const canDelete = code.status === 'active' return ( - {code.code} + {codeLabel} {formatMoney(code.amount, code.currency)} - {code.redemptionCount} {code.expiresAt ? new Date(code.expiresAt).toLocaleString() : '-'} @@ -256,7 +257,7 @@ function CodeRow({