Add frontend/modules/loyalty with types, API client, design system, feature pages and thin App Router routes under app/loyalty/. BFF proxy at app/api/loyalty/. Include loyalty frontend docs. Co-authored-by: Cursor <cursoragent@cursor.com>
558 lines
20 KiB
TypeScript
558 lines
20 KiB
TypeScript
/**
|
||
* Loyalty Platform Service API client — real backend only (Phase 7.0–7.6).
|
||
* Browser → /api/loyalty BFF; SSR → direct upstream.
|
||
*/
|
||
|
||
import { getValidAccessToken, refreshSession } from "@/lib/auth";
|
||
import type {
|
||
AuditEntry,
|
||
Campaign,
|
||
CampaignApplication,
|
||
CampaignApplicationPage,
|
||
HealthResponse,
|
||
ListParams,
|
||
Member,
|
||
MembershipLifecycleEvent,
|
||
MembershipTier,
|
||
LoyaltyProgram,
|
||
PointAccount,
|
||
PointBalance,
|
||
PointLedgerEntry,
|
||
PointLedgerPage,
|
||
ReferralAttribution,
|
||
ReferralAttributionPage,
|
||
ReferralCode,
|
||
ReferralCodePage,
|
||
ReferralProgram,
|
||
Reward,
|
||
RewardRedemption,
|
||
RewardRedemptionPage,
|
||
WalletAccount,
|
||
WalletBalance,
|
||
WalletLedgerEntry,
|
||
WalletLedgerPage,
|
||
WalletTransferResult,
|
||
} from "@/modules/loyalty/types";
|
||
|
||
const LOYALTY_BASE =
|
||
typeof window !== "undefined"
|
||
? "/api/loyalty"
|
||
: process.env.LOYALTY_SERVICE_URL ||
|
||
process.env.NEXT_PUBLIC_LOYALTY_API_URL ||
|
||
"http://localhost:8004";
|
||
|
||
export class LoyaltyApiError extends Error {
|
||
constructor(
|
||
public status: number,
|
||
public code: string,
|
||
message: string
|
||
) {
|
||
super(message);
|
||
this.name = "LoyaltyApiError";
|
||
}
|
||
}
|
||
|
||
type RequestOptions = RequestInit & { tenantId: string; auth?: boolean };
|
||
|
||
async function request<T>(path: string, options: RequestOptions): Promise<T> {
|
||
const { tenantId, auth = true, headers: customHeaders, ...init } = options;
|
||
const headers = new Headers(customHeaders);
|
||
if (!headers.has("Content-Type") && init.body) {
|
||
headers.set("Content-Type", "application/json");
|
||
}
|
||
headers.set("X-Tenant-ID", tenantId);
|
||
if (auth) {
|
||
const token = await getValidAccessToken();
|
||
if (token) headers.set("Authorization", `Bearer ${token}`);
|
||
}
|
||
|
||
const url = path.startsWith("http")
|
||
? path
|
||
: `${LOYALTY_BASE.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`;
|
||
|
||
let res: Response;
|
||
try {
|
||
res = await fetch(url, { ...init, headers });
|
||
} catch (err) {
|
||
const detail = err instanceof Error ? err.message : "network_error";
|
||
throw new Error(`اتصال به سرویس وفاداری برقرار نشد (${url}) — ${detail}`);
|
||
}
|
||
|
||
if (res.status === 401 && auth) {
|
||
const refreshed = await refreshSession({ force: true });
|
||
if (refreshed) {
|
||
headers.set("Authorization", `Bearer ${refreshed}`);
|
||
res = await fetch(url, { ...init, headers });
|
||
}
|
||
}
|
||
|
||
if (!res.ok) {
|
||
const body = await res.json().catch(() => ({}));
|
||
throw new LoyaltyApiError(
|
||
res.status,
|
||
body?.error?.code || body?.detail || "unknown_error",
|
||
body?.error?.message || body?.detail || res.statusText
|
||
);
|
||
}
|
||
if (res.status === 204) return undefined as T;
|
||
return res.json() as Promise<T>;
|
||
}
|
||
|
||
function qs(params?: Record<string, string | number | boolean | undefined | null>) {
|
||
if (!params) return "";
|
||
const sp = new URLSearchParams();
|
||
Object.entries(params).forEach(([k, v]) => {
|
||
if (v !== undefined && v !== null && v !== "") sp.set(k, String(v));
|
||
});
|
||
const s = sp.toString();
|
||
return s ? `?${s}` : "";
|
||
}
|
||
|
||
function apiPath(segment: string) {
|
||
return `/api/v1${segment.startsWith("/") ? segment : `/${segment}`}`;
|
||
}
|
||
|
||
const ZERO_UUID = "00000000-0000-0000-0000-000000000000";
|
||
|
||
export type LedgerListParams = {
|
||
offset?: number;
|
||
limit?: number;
|
||
entry_type?: string;
|
||
q?: string;
|
||
};
|
||
|
||
export type WalletLedgerListParams = {
|
||
offset?: number;
|
||
limit?: number;
|
||
entry_type?: string;
|
||
};
|
||
|
||
export const loyaltyApi = {
|
||
health: () =>
|
||
request<HealthResponse>("/health", { tenantId: ZERO_UUID, auth: false }),
|
||
|
||
programs: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<LoyaltyProgram[]>(`${apiPath("/programs")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<LoyaltyProgram>(apiPath(`/programs/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<LoyaltyProgram>(apiPath("/programs"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<LoyaltyProgram>(apiPath(`/programs/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<LoyaltyProgram>(apiPath(`/programs/${id}/delete`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
},
|
||
|
||
tiers: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<MembershipTier[]>(`${apiPath("/tiers")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<MembershipTier>(apiPath(`/tiers/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<MembershipTier>(apiPath("/tiers"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<MembershipTier>(apiPath(`/tiers/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<MembershipTier>(apiPath(`/tiers/${id}/delete`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
},
|
||
|
||
members: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Member[]>(`${apiPath("/members")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<Member>(apiPath(`/members/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath("/members"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath(`/members/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
enroll: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath(`/members/${id}/enroll`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
activate: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath(`/members/${id}/activate`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
renew: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath(`/members/${id}/renew`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
freeze: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath(`/members/${id}/freeze`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
resume: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath(`/members/${id}/resume`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
cancel: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath(`/members/${id}/cancel`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
expire: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath(`/members/${id}/expire`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
transfer: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Member>(apiPath(`/members/${id}/transfer`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
lifecycle: (tenantId: string, id: string, limit?: number) =>
|
||
request<MembershipLifecycleEvent[]>(
|
||
`${apiPath(`/members/${id}/lifecycle`)}${qs({ limit })}`,
|
||
{ tenantId }
|
||
),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<Member>(apiPath(`/members/${id}/delete`), { tenantId, method: "POST" }),
|
||
},
|
||
|
||
pointAccounts: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<PointAccount[]>(`${apiPath("/point-accounts")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<PointAccount>(apiPath(`/point-accounts/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<PointAccount>(apiPath("/point-accounts"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<PointAccount>(apiPath(`/point-accounts/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<PointAccount>(apiPath(`/point-accounts/${id}/delete`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
balance: (tenantId: string, id: string) =>
|
||
request<PointBalance>(apiPath(`/point-accounts/${id}/balance`), { tenantId }),
|
||
ledger: (tenantId: string, id: string, params?: LedgerListParams) =>
|
||
request<PointLedgerPage>(`${apiPath(`/point-accounts/${id}/ledger`)}${qs(params)}`, {
|
||
tenantId,
|
||
}),
|
||
earn: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<PointLedgerEntry>(apiPath(`/point-accounts/${id}/earn`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
redeem: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<PointLedgerEntry>(apiPath(`/point-accounts/${id}/redeem`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
adjust: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<PointLedgerEntry>(apiPath(`/point-accounts/${id}/adjust`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
expirePoints: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<PointLedgerEntry>(apiPath(`/point-accounts/${id}/expire-points`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
rewards: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Reward[]>(`${apiPath("/rewards")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<Reward>(apiPath(`/rewards/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Reward>(apiPath("/rewards"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Reward>(apiPath(`/rewards/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<Reward>(apiPath(`/rewards/${id}/delete`), { tenantId, method: "POST" }),
|
||
redeem: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<RewardRedemption>(apiPath(`/rewards/${id}/redeem`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
redemptions: (tenantId: string, id: string, params?: ListParams) =>
|
||
request<RewardRedemptionPage>(
|
||
`${apiPath(`/rewards/${id}/redemptions`)}${qs(params)}`,
|
||
{ tenantId }
|
||
),
|
||
},
|
||
|
||
redemptions: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<RewardRedemptionPage>(`${apiPath("/redemptions")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<RewardRedemption>(apiPath(`/redemptions/${id}`), { tenantId }),
|
||
fulfill: (tenantId: string, id: string) =>
|
||
request<RewardRedemption>(apiPath(`/redemptions/${id}/fulfill`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
cancel: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<RewardRedemption>(apiPath(`/redemptions/${id}/cancel`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
campaigns: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<Campaign[]>(`${apiPath("/campaigns")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<Campaign>(apiPath(`/campaigns/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<Campaign>(apiPath("/campaigns"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Campaign>(apiPath(`/campaigns/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<Campaign>(apiPath(`/campaigns/${id}/delete`), { tenantId, method: "POST" }),
|
||
schedule: (tenantId: string, id: string) =>
|
||
request<Campaign>(apiPath(`/campaigns/${id}/schedule`), { tenantId, method: "POST" }),
|
||
activate: (tenantId: string, id: string) =>
|
||
request<Campaign>(apiPath(`/campaigns/${id}/activate`), { tenantId, method: "POST" }),
|
||
pause: (tenantId: string, id: string) =>
|
||
request<Campaign>(apiPath(`/campaigns/${id}/pause`), { tenantId, method: "POST" }),
|
||
resume: (tenantId: string, id: string) =>
|
||
request<Campaign>(apiPath(`/campaigns/${id}/resume`), { tenantId, method: "POST" }),
|
||
expire: (tenantId: string, id: string) =>
|
||
request<Campaign>(apiPath(`/campaigns/${id}/expire`), { tenantId, method: "POST" }),
|
||
rules: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<Campaign>(apiPath(`/campaigns/${id}/rules`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
apply: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<CampaignApplication>(apiPath(`/campaigns/${id}/apply`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
applications: (tenantId: string, id: string, params?: ListParams) =>
|
||
request<CampaignApplicationPage>(
|
||
`${apiPath(`/campaigns/${id}/applications`)}${qs(params)}`,
|
||
{ tenantId }
|
||
),
|
||
},
|
||
|
||
referralPrograms: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<ReferralProgram[]>(`${apiPath("/referral-programs")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<ReferralProgram>(apiPath(`/referral-programs/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<ReferralProgram>(apiPath("/referral-programs"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<ReferralProgram>(apiPath(`/referral-programs/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<ReferralProgram>(apiPath(`/referral-programs/${id}/delete`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
},
|
||
|
||
referralCodes: {
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<ReferralCode>(apiPath("/referral-codes"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
list: (tenantId: string, params?: ListParams & { referral_program_id?: string }) =>
|
||
request<ReferralCodePage>(`${apiPath("/referral-codes")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<ReferralCode>(apiPath(`/referral-codes/${id}`), { tenantId }),
|
||
revoke: (tenantId: string, id: string) =>
|
||
request<ReferralCode>(apiPath(`/referral-codes/${id}/revoke`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
},
|
||
|
||
referrals: {
|
||
attribute: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<ReferralAttribution>(apiPath("/referrals/attribute"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
list: (tenantId: string, params?: ListParams & { referral_program_id?: string }) =>
|
||
request<ReferralAttributionPage>(`${apiPath("/referrals")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<ReferralAttribution>(apiPath(`/referrals/${id}`), { tenantId }),
|
||
convert: (tenantId: string, id: string) =>
|
||
request<ReferralAttribution>(apiPath(`/referrals/${id}/convert`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
reward: (tenantId: string, id: string) =>
|
||
request<ReferralAttribution>(apiPath(`/referrals/${id}/reward`), {
|
||
tenantId,
|
||
method: "POST",
|
||
}),
|
||
cancel: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<ReferralAttribution>(apiPath(`/referrals/${id}/cancel`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
wallets: {
|
||
list: (tenantId: string, params?: ListParams) =>
|
||
request<WalletAccount[]>(`${apiPath("/wallets")}${qs(params)}`, { tenantId }),
|
||
get: (tenantId: string, id: string) =>
|
||
request<WalletAccount>(apiPath(`/wallets/${id}`), { tenantId }),
|
||
create: (tenantId: string, body: Record<string, unknown>) =>
|
||
request<WalletAccount>(apiPath("/wallets"), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
update: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<WalletAccount>(apiPath(`/wallets/${id}`), {
|
||
tenantId,
|
||
method: "PATCH",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
delete: (tenantId: string, id: string) =>
|
||
request<WalletAccount>(apiPath(`/wallets/${id}/delete`), { tenantId, method: "POST" }),
|
||
balance: (tenantId: string, id: string) =>
|
||
request<WalletBalance>(apiPath(`/wallets/${id}/balance`), { tenantId }),
|
||
ledger: (tenantId: string, id: string, params?: WalletLedgerListParams) =>
|
||
request<WalletLedgerPage>(`${apiPath(`/wallets/${id}/ledger`)}${qs(params)}`, { tenantId }),
|
||
credit: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<WalletLedgerEntry>(apiPath(`/wallets/${id}/credit`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
debit: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<WalletLedgerEntry>(apiPath(`/wallets/${id}/debit`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
adjust: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<WalletLedgerEntry>(apiPath(`/wallets/${id}/adjust`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
transfer: (tenantId: string, id: string, body: Record<string, unknown>) =>
|
||
request<WalletTransferResult>(apiPath(`/wallets/${id}/transfer`), {
|
||
tenantId,
|
||
method: "POST",
|
||
body: JSON.stringify(body),
|
||
}),
|
||
},
|
||
|
||
audit: {
|
||
list: (
|
||
tenantId: string,
|
||
entityType: string,
|
||
entityId: string,
|
||
params?: { limit?: number }
|
||
) =>
|
||
request<AuditEntry[]>(
|
||
`${apiPath("/audit")}${qs({ entity_type: entityType, entity_id: entityId, ...params })}`,
|
||
{ tenantId }
|
||
),
|
||
},
|
||
};
|
||
|
||
/** Fetch all pages client-side (backend has no total count on plain list endpoints). */
|
||
export async function loyaltyFetchAll<T>(
|
||
fetchPage: (params: ListParams) => Promise<T[]>,
|
||
pageSize = 500
|
||
): Promise<T[]> {
|
||
const all: T[] = [];
|
||
let page = 1;
|
||
for (;;) {
|
||
const batch = await fetchPage({ page, page_size: pageSize });
|
||
all.push(...batch);
|
||
if (batch.length < pageSize) break;
|
||
page += 1;
|
||
if (page > 50) break;
|
||
}
|
||
return all;
|
||
}
|