mirror of
https://github.com/saltbo/zpan.git
synced 2026-08-30 17:50:07 +08:00
fix(cloud-store): align gift cards with cloud model
This commit is contained in:
+6
-11
@@ -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<CloudGiftCardPage>(page, '/api/admin/store/gift-cards', {
|
||||
const cards = await postJson<CloudGiftCard[]>(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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<typeof getRequiredSettings>[0]) {
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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<Env>()
|
||||
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)
|
||||
})
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -196,7 +196,7 @@ function CodeStatusSelect({ status, onStatusChange }: Pick<CodeListProps, 'statu
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(['all', 'created', 'active', 'disabled', 'exhausted', 'expired', 'revoked'] as const).map((value) => (
|
||||
{(['all', 'active', 'redeemed', 'disabled', 'expired', 'revoked'] as const).map((value) => (
|
||||
<SelectItem key={value} value={value}>
|
||||
{t(`admin.cloudStore.codes.status.${value}`)}
|
||||
</SelectItem>
|
||||
@@ -213,7 +213,7 @@ function CodeTable(props: CodeListProps) {
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{['code', 'storage', 'uses', 'expires', 'statusLabel'].map((key) => (
|
||||
{['code', 'storage', 'expires', 'statusLabel'].map((key) => (
|
||||
<TableHead key={key}>{t(`admin.cloudStore.codes.${key}`)}</TableHead>
|
||||
))}
|
||||
<TableHead className="text-right">{t('common.actions')}</TableHead>
|
||||
@@ -221,7 +221,7 @@ function CodeTable(props: CodeListProps) {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{props.codes.map((code) => (
|
||||
<CodeRow key={code.code} code={code} {...props} />
|
||||
<CodeRow key={code.id} code={code} {...props} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -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 (
|
||||
<TableRow>
|
||||
<TableCell className="font-mono text-xs">{code.code}</TableCell>
|
||||
<TableCell className="font-mono text-xs">{codeLabel}</TableCell>
|
||||
<TableCell>{formatMoney(code.amount, code.currency)}</TableCell>
|
||||
<TableCell>{code.redemptionCount}</TableCell>
|
||||
<TableCell>{code.expiresAt ? new Date(code.expiresAt).toLocaleString() : '-'}</TableCell>
|
||||
<TableCell>
|
||||
<CodeStatusBadge code={code} />
|
||||
@@ -256,7 +257,7 @@ function CodeRow({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!available || !canDisable || disablingGiftCard === code.code}
|
||||
disabled={!available || !canDisable || disablingGiftCard === actionTarget}
|
||||
onClick={() => setRevokeConfirmOpen(true)}
|
||||
>
|
||||
<Ban className="mr-2 h-4 w-4" />
|
||||
@@ -265,7 +266,7 @@ function CodeRow({
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={!available || !canDelete || deletingGiftCard === code.code}
|
||||
disabled={!available || !canDelete || deletingGiftCard === actionTarget}
|
||||
onClick={() => setDeleteConfirmOpen(true)}
|
||||
>
|
||||
<Trash2 className="mr-2 h-4 w-4" />
|
||||
@@ -276,21 +277,21 @@ function CodeRow({
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admin.cloudStore.codes.disableTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('admin.cloudStore.codes.disableConfirm', { code: code.code })}</DialogDescription>
|
||||
<DialogDescription>{t('admin.cloudStore.codes.disableConfirm', { code: codeLabel })}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={disablingGiftCard === code.code}
|
||||
disabled={disablingGiftCard === actionTarget}
|
||||
onClick={() => setRevokeConfirmOpen(false)}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={disablingGiftCard === code.code}
|
||||
disabled={disablingGiftCard === actionTarget}
|
||||
onClick={() => {
|
||||
onRevoke(code.code)
|
||||
onRevoke(actionTarget)
|
||||
setRevokeConfirmOpen(false)
|
||||
}}
|
||||
>
|
||||
@@ -303,21 +304,21 @@ function CodeRow({
|
||||
<DialogContent className="sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('admin.cloudStore.codes.deleteTitle')}</DialogTitle>
|
||||
<DialogDescription>{t('admin.cloudStore.codes.deleteConfirm', { code: code.code })}</DialogDescription>
|
||||
<DialogDescription>{t('admin.cloudStore.codes.deleteConfirm', { code: codeLabel })}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
disabled={deletingGiftCard === code.code}
|
||||
disabled={deletingGiftCard === actionTarget}
|
||||
onClick={() => setDeleteConfirmOpen(false)}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={deletingGiftCard === code.code}
|
||||
disabled={deletingGiftCard === actionTarget}
|
||||
onClick={() => {
|
||||
onDelete(code.code)
|
||||
onDelete(actionTarget)
|
||||
setDeleteConfirmOpen(false)
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -1116,10 +1116,9 @@
|
||||
"admin.cloudStore.codes.expires": "Expires",
|
||||
"admin.cloudStore.codes.statusLabel": "Status",
|
||||
"admin.cloudStore.codes.status.all": "All",
|
||||
"admin.cloudStore.codes.status.created": "Created",
|
||||
"admin.cloudStore.codes.status.active": "Active",
|
||||
"admin.cloudStore.codes.status.redeemed": "Redeemed",
|
||||
"admin.cloudStore.codes.status.disabled": "Disabled",
|
||||
"admin.cloudStore.codes.status.exhausted": "Exhausted",
|
||||
"admin.cloudStore.codes.status.expired": "Expired",
|
||||
"admin.cloudStore.codes.status.revoked": "Revoked",
|
||||
"admin.cloudStore.codes.empty": "No gift cards match this filter.",
|
||||
|
||||
@@ -1116,10 +1116,9 @@
|
||||
"admin.cloudStore.codes.expires": "过期时间",
|
||||
"admin.cloudStore.codes.statusLabel": "状态",
|
||||
"admin.cloudStore.codes.status.all": "全部",
|
||||
"admin.cloudStore.codes.status.created": "未使用",
|
||||
"admin.cloudStore.codes.status.active": "可用",
|
||||
"admin.cloudStore.codes.status.redeemed": "已兑换",
|
||||
"admin.cloudStore.codes.status.disabled": "已停用",
|
||||
"admin.cloudStore.codes.status.exhausted": "已用完",
|
||||
"admin.cloudStore.codes.status.expired": "已失效",
|
||||
"admin.cloudStore.codes.status.revoked": "已撤销",
|
||||
"admin.cloudStore.codes.empty": "没有匹配此筛选条件的礼品卡。",
|
||||
|
||||
+2
-2
@@ -408,7 +408,7 @@ describe('api', () => {
|
||||
it('calls admin gift card and order endpoints', async () => {
|
||||
vi.mocked(fetch)
|
||||
.mockResolvedValueOnce(makeResponse({ items: [], total: 0 }))
|
||||
.mockResolvedValueOnce(makeResponse({ items: [{ code: 'ZS123' }], total: 1 }))
|
||||
.mockResolvedValueOnce(makeResponse([{ code: 'ZS123' }]))
|
||||
.mockResolvedValueOnce(makeResponse({ code: 'ZS123', disabled: true }))
|
||||
.mockResolvedValueOnce(makeResponse({ code: 'ZS123', deleted: true }))
|
||||
.mockResolvedValueOnce(makeResponse({ items: [], total: 0 }))
|
||||
@@ -429,7 +429,7 @@ describe('api', () => {
|
||||
currency: 'usd',
|
||||
count: 3,
|
||||
})
|
||||
expect(createdGiftCards).toEqual({ items: [{ code: 'ZS123' }], total: 1 })
|
||||
expect(createdGiftCards).toEqual([{ code: 'ZS123' }])
|
||||
expect(calls[2][0]).toBe('/api/admin/store/gift-cards/ZS123')
|
||||
expect(calls[2][1].method).toBe('PATCH')
|
||||
expect(JSON.parse(calls[2][1].body as string)).toEqual({ disabled: true })
|
||||
|
||||
+1
-1
@@ -361,7 +361,7 @@ export function listCloudGiftCards(status?: GiftCardStatus) {
|
||||
}
|
||||
|
||||
export function createCloudGiftCards(data: CreateGiftCardInput) {
|
||||
return unwrap<{ items: CloudGiftCard[]; total: number }>(adminCloudStoreApi['gift-cards'].$post({ json: data }))
|
||||
return unwrap<CloudGiftCard[]>(adminCloudStoreApi['gift-cards'].$post({ json: data }))
|
||||
}
|
||||
|
||||
export function disableCloudGiftCard(code: string) {
|
||||
|
||||
@@ -108,15 +108,14 @@ function quotaPackage(overrides: Partial<CloudProduct> = {}): CloudProduct {
|
||||
function giftCard(overrides: Partial<CloudGiftCard> = {}): CloudGiftCard {
|
||||
return {
|
||||
id: 'gift-card-1',
|
||||
boundLicenseId: null,
|
||||
code: 'ZS-CODE-1',
|
||||
storeId: 'store-1',
|
||||
campaignId: null,
|
||||
code: null,
|
||||
codeLast4: 'ODE1',
|
||||
amount: 10_000,
|
||||
currency: 'usd',
|
||||
status: 'created',
|
||||
status: 'active',
|
||||
expiresAt: null,
|
||||
firstRedeemedAt: null,
|
||||
lastRedeemedAt: null,
|
||||
redemptionCount: 0,
|
||||
disabledAt: null,
|
||||
revokedAt: null,
|
||||
createdAt: '2026-05-05T00:00:00.000Z',
|
||||
@@ -496,14 +495,14 @@ describe('AdminCloudStorePage', () => {
|
||||
items: [giftCard()],
|
||||
total: 1,
|
||||
})
|
||||
vi.mocked(createCloudGiftCards).mockResolvedValue({ items: [], total: 0 })
|
||||
vi.mocked(disableCloudGiftCard).mockResolvedValue({ code: 'ZS-CODE-1', disabled: true })
|
||||
vi.mocked(createCloudGiftCards).mockResolvedValue([])
|
||||
vi.mocked(disableCloudGiftCard).mockResolvedValue({ code: 'gift-card-1', disabled: true })
|
||||
|
||||
const view = renderAdminPage()
|
||||
|
||||
await waitFor(() => expect(view.getByRole('tab', { name: 'admin.cloudStore.tabs.codes' })).toBeTruthy())
|
||||
fireEvent.click(view.getByRole('tab', { name: 'admin.cloudStore.tabs.codes' }))
|
||||
await waitFor(() => expect(view.getByText('ZS-CODE-1')).toBeTruthy())
|
||||
await waitFor(() => expect(view.getByText('ODE1')).toBeTruthy())
|
||||
fireEvent.click(view.getByRole('button', { name: 'admin.cloudStore.codes.generateTitle' }))
|
||||
const dialog = await view.findByRole('dialog')
|
||||
fireEvent.change(view.getByLabelText('admin.cloudStore.codes.amount'), { target: { value: '50' } })
|
||||
@@ -526,7 +525,7 @@ describe('AdminCloudStorePage', () => {
|
||||
expect(within(disableDialog).getByText('admin.cloudStore.codes.disableTitle')).toBeTruthy()
|
||||
fireEvent.click(within(disableDialog).getByRole('button', { name: 'admin.cloudStore.codes.disable' }))
|
||||
|
||||
await waitFor(() => expect(vi.mocked(disableCloudGiftCard).mock.calls[0][0]).toBe('ZS-CODE-1'))
|
||||
await waitFor(() => expect(vi.mocked(disableCloudGiftCard).mock.calls[0][0]).toBe('gift-card-1'))
|
||||
})
|
||||
|
||||
it('deletes an eligible gift card from the codes tab', async () => {
|
||||
@@ -536,13 +535,13 @@ describe('AdminCloudStorePage', () => {
|
||||
items: [giftCard()],
|
||||
total: 1,
|
||||
})
|
||||
vi.mocked(deleteCloudGiftCard).mockResolvedValue({ code: 'ZS-CODE-1', deleted: true })
|
||||
vi.mocked(deleteCloudGiftCard).mockResolvedValue({ code: 'gift-card-1', deleted: true })
|
||||
|
||||
const view = renderAdminPage()
|
||||
|
||||
await waitFor(() => expect(view.getByRole('tab', { name: 'admin.cloudStore.tabs.codes' })).toBeTruthy())
|
||||
fireEvent.click(view.getByRole('tab', { name: 'admin.cloudStore.tabs.codes' }))
|
||||
await waitFor(() => expect(view.getByText('ZS-CODE-1')).toBeTruthy())
|
||||
await waitFor(() => expect(view.getByText('ODE1')).toBeTruthy())
|
||||
|
||||
fireEvent.click(view.getByRole('button', { name: 'admin.cloudStore.codes.delete' }))
|
||||
expect(deleteCloudGiftCard).not.toHaveBeenCalled()
|
||||
@@ -551,14 +550,14 @@ describe('AdminCloudStorePage', () => {
|
||||
expect(within(deleteDialog).getByText('admin.cloudStore.codes.deleteTitle')).toBeTruthy()
|
||||
fireEvent.click(within(deleteDialog).getByRole('button', { name: 'admin.cloudStore.codes.delete' }))
|
||||
|
||||
await waitFor(() => expect(vi.mocked(deleteCloudGiftCard).mock.calls[0][0]).toBe('ZS-CODE-1'))
|
||||
await waitFor(() => expect(vi.mocked(deleteCloudGiftCard).mock.calls[0][0]).toBe('gift-card-1'))
|
||||
})
|
||||
|
||||
it('shows gift cards in a table and opens generation fields in a dialog', async () => {
|
||||
vi.mocked(getCloudStoreSettings).mockResolvedValue(settings())
|
||||
vi.mocked(listAdminCloudProducts).mockResolvedValue({ items: [], total: 0 })
|
||||
vi.mocked(listCloudGiftCards).mockResolvedValue({
|
||||
items: [giftCard({ id: 'gift-card-2', code: 'ZS-CODE-2', amount: 5000, redemptionCount: 2, status: 'active' })],
|
||||
items: [giftCard({ id: 'gift-card-2', code: null, codeLast4: 'ODE2', amount: 5000, status: 'active' })],
|
||||
total: 1,
|
||||
})
|
||||
|
||||
@@ -567,7 +566,7 @@ describe('AdminCloudStorePage', () => {
|
||||
await waitFor(() => expect(view.getByRole('tab', { name: 'admin.cloudStore.tabs.codes' })).toBeTruthy())
|
||||
fireEvent.click(view.getByRole('tab', { name: 'admin.cloudStore.tabs.codes' }))
|
||||
|
||||
await waitFor(() => expect(view.getByText('ZS-CODE-2')).toBeTruthy())
|
||||
await waitFor(() => expect(view.getByText('ODE2')).toBeTruthy())
|
||||
expect(view.getByRole('table')).toBeTruthy()
|
||||
expect(view.getByRole('columnheader', { name: 'admin.cloudStore.codes.code' })).toBeTruthy()
|
||||
expect(view.queryByLabelText('admin.cloudStore.codes.count')).toBeNull()
|
||||
|
||||
Reference in New Issue
Block a user