2026-09-18 12:54:45 +03:30
|
|
|
import type { CreateOrderDto, CustomerBalanceDto } from '@gold-bank/shared-types';
|
|
|
|
|
|
|
|
|
|
const BASE = '/api';
|
|
|
|
|
|
|
|
|
|
async function request<T>(path: string, options?: RequestInit): Promise<T> {
|
|
|
|
|
const res = await fetch(`${BASE}${path}`, {
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
...options,
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
const body = await res.text();
|
|
|
|
|
throw new Error(`API ${path} failed (${res.status}): ${body}`);
|
|
|
|
|
}
|
|
|
|
|
return res.json();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ProductVariantSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string;
|
|
|
|
|
rashWeightGrams: string;
|
|
|
|
|
defaultWagePercent: string;
|
|
|
|
|
/// On-hand stock and the "orderable at zero stock" flag. Both exist on
|
|
|
|
|
/// ProductVariant and are already returned by GET /catalog/products (the
|
|
|
|
|
/// catalog service includes every variant field) — declared here so the UI
|
|
|
|
|
/// can read them.
|
|
|
|
|
qty?: number;
|
|
|
|
|
isPreOrder?: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CategorySummary {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string;
|
|
|
|
|
parentId: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface BrandSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CollectionSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string;
|
|
|
|
|
description: string | null;
|
|
|
|
|
isActive: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface ProductSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
title: string;
|
|
|
|
|
description: string | null;
|
|
|
|
|
/// Legacy Product.product_code — the human-facing code. Already returned by
|
|
|
|
|
/// GET /catalog/products.
|
|
|
|
|
productCode?: string | null;
|
|
|
|
|
variants: ProductVariantSummary[];
|
|
|
|
|
images: Array<{ url: string }>;
|
|
|
|
|
category: CategorySummary | null;
|
|
|
|
|
brand: BrandSummary | null;
|
|
|
|
|
collections: Array<{ collectionId: string; collection: CollectionSummary }>;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface LedgerEntrySummary {
|
|
|
|
|
id: string;
|
|
|
|
|
currency: string;
|
|
|
|
|
direction: string;
|
|
|
|
|
sourceType: string;
|
|
|
|
|
amount: string;
|
|
|
|
|
confirmed: boolean;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
note: string | null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-22 00:43:11 +03:30
|
|
|
/**
|
|
|
|
|
* What `GET /customers` returns — the login lookup, and nothing more.
|
|
|
|
|
*
|
|
|
|
|
* It used to carry fullName, national ID, both debts and the credit limit. The
|
|
|
|
|
* route is unauthenticated (the login screen matches the phone number in the
|
|
|
|
|
* browser), so that shipped the whole customer book to any visitor; the API now
|
|
|
|
|
* selects only these four fields. See SEC-08 in docs/parity/ISSUES.md.
|
|
|
|
|
*
|
|
|
|
|
* The route disappears altogether once OTP verify returns the one authenticated
|
|
|
|
|
* customer, the way the reference did it.
|
|
|
|
|
*/
|
2026-09-18 12:54:45 +03:30
|
|
|
export interface CustomerSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
mobile: string;
|
2026-09-21 17:37:26 +03:30
|
|
|
isActive: boolean;
|
|
|
|
|
isArchived: boolean;
|
2026-09-18 12:54:45 +03:30
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface OrderSummary {
|
|
|
|
|
id: string;
|
|
|
|
|
orderNumber: string;
|
|
|
|
|
status: string;
|
|
|
|
|
totalGoldWeightGrams: string;
|
|
|
|
|
isSuspendedOverCreditLimit: boolean;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface OrderItemDetail {
|
|
|
|
|
id: string;
|
|
|
|
|
productVariantId: string;
|
|
|
|
|
quantity: number;
|
|
|
|
|
rashWeightGramsSnapshot: string;
|
|
|
|
|
wagePercentSnapshot: string;
|
|
|
|
|
discountPercentSnapshot: string;
|
|
|
|
|
computedWeightGrams: string;
|
|
|
|
|
flatRialCharge: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface OrderDetail {
|
|
|
|
|
id: string;
|
|
|
|
|
orderNumber: string;
|
|
|
|
|
status: string;
|
|
|
|
|
totalGoldWeightGrams: string;
|
|
|
|
|
wageWeightGrams: string;
|
|
|
|
|
flatRialCharge: string;
|
|
|
|
|
isSuspendedOverCreditLimit: boolean;
|
|
|
|
|
deliveredAt: string | null;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
items: OrderItemDetail[];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CustomerDetail {
|
|
|
|
|
id: string;
|
|
|
|
|
fullName: string;
|
|
|
|
|
mobile: string;
|
|
|
|
|
nationalCode: string;
|
|
|
|
|
cityId: string | null;
|
|
|
|
|
goldCreditLimitGrams: string;
|
|
|
|
|
goldDebtGrams: string;
|
|
|
|
|
fiatDebtRial: string;
|
|
|
|
|
verificationStep: string;
|
|
|
|
|
createdAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GoldPriceQuote {
|
|
|
|
|
id: string;
|
|
|
|
|
karatPerMille: number;
|
|
|
|
|
pricePerGramRial: string;
|
|
|
|
|
source: string;
|
|
|
|
|
quotedAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export const api = {
|
|
|
|
|
listProducts: () => request<ProductSummary[]>('/catalog/products'),
|
|
|
|
|
listCustomers: () => request<CustomerSummary[]>('/customers'),
|
|
|
|
|
getCustomer: (id: string) => request<CustomerDetail>(`/customers/${id}`),
|
|
|
|
|
getBalance: (customerId: string) => request<CustomerBalanceDto>(`/accounting/customers/${customerId}/balance`),
|
|
|
|
|
getLedgerEntries: (customerId: string) => request<LedgerEntrySummary[]>(`/customers/${customerId}/ledger-entries`),
|
|
|
|
|
createOrder: (dto: CreateOrderDto) => request<OrderSummary>('/orders', { method: 'POST', body: JSON.stringify(dto) }),
|
|
|
|
|
getCustomerOrders: (customerId: string) => request<OrderDetail[]>(`/customers/${customerId}/orders`),
|
|
|
|
|
getLatestGoldPrice: (karatPerMille = 750) => request<GoldPriceQuote>(`/pricing/latest?karatPerMille=${karatPerMille}`),
|
|
|
|
|
};
|