feat(freight): shop freight templates, server-side checkout fees, company dictionary (add-freight-templates)

This commit is contained in:
Chengdong Zhang
2026-09-24 15:03:51 +08:00
parent 5b426486ac
commit 473d19d089
44 changed files with 2409 additions and 67 deletions
+24 -1
View File
@@ -45,7 +45,11 @@ import type {
ProductStatus,
PublicFlashSaleSession,
RedeemPointsBody,
FreightTemplate,
FreightTemplateInput,
Shipment,
ShippingCompany,
ShippingQuote,
Shop,
ShopFlashSaleSession,
ShopProfile,
@@ -87,6 +91,7 @@ export interface ProductListQuery {
export interface ProductUpsertBody {
category_id?: string | null;
brand_id?: string | null;
freight_template_id?: string | null;
slug: string;
name: LocalizedText;
description?: LocalizedText;
@@ -100,6 +105,7 @@ export interface SkuUpsertBody {
currency: string;
stock: number;
active?: boolean;
weight_grams?: number | null;
}
export interface ShipmentItemBody {
@@ -248,6 +254,10 @@ export interface ApiClient {
addAftersaleMessage(id: string, body: AftersaleMessageBody): Promise<AftersaleMessage>;
/** Public points catalog: published products only. */
listPointsProducts(): Promise<IntegralProduct[]>;
/** Server-computed per-shop shipping fees for the current cart + address. */
quoteShipping(address: Address, currency: string): Promise<ShippingQuote>;
/** Public shipping-company dictionary. */
listShippingCompanies(): Promise<ShippingCompany[]>;
listMyRedemptions(): Promise<IntegralOrder[]>;
redeemPoints(body: RedeemPointsBody): Promise<IntegralOrder>;
/** Public: active flash-sale sessions with their purchasable items. */
@@ -272,6 +282,7 @@ export interface ApiClient {
carrier: string,
trackingNo: string,
items: ShipmentItemBody[],
shippingCompanyCode?: string,
): Promise<Shipment>;
listShipments(): Promise<Shipment[]>;
markShipped(id: string): Promise<Shipment>;
@@ -302,6 +313,10 @@ export interface ApiClient {
confirmAftersaleReceipt(id: string): Promise<Aftersale>;
refundAftersale(id: string): Promise<Aftersale>;
addAftersaleMessage(id: string, body: AftersaleMessageBody): Promise<AftersaleMessage>;
listFreightTemplates(): Promise<FreightTemplate[]>;
createFreightTemplate(body: FreightTemplateInput): Promise<FreightTemplate>;
updateFreightTemplate(id: string, body: FreightTemplateInput): Promise<FreightTemplate>;
deleteFreightTemplate(id: string): Promise<void>;
};
admin: {
listUsers(page?: number): Promise<Paged<User>>;
@@ -359,6 +374,9 @@ export function createApi(opts: ApiClientOptions): ApiClient {
payOrder: (id) => r("POST", `/orders/${id}/pay`),
confirmDelivered: (shipmentId) => r("POST", `/shipments/${shipmentId}/confirm-delivered`),
listMyShipments: () => r("GET", "/shipments"),
quoteShipping: (address, currency) =>
r("POST", "/orders/shipping-quote", { shipping_address: address, currency }),
listShippingCompanies: () => r("GET", "/shipping/companies"),
requestInvoice: (orderId, title, taxNo, kind) =>
r("POST", `/orders/${orderId}/invoice`, { title, tax_no: taxNo, kind }),
listMyInvoices: () => r("GET", "/invoices"),
@@ -403,10 +421,11 @@ export function createApi(opts: ApiClientOptions): ApiClient {
upsertSku: (productId, body) => r("POST", `/shop/products/${productId}/skus`, body),
listOrders: (q = {}) => r("GET", "/shop/orders", undefined, { ...q }),
getOrder: (id) => r("GET", `/shop/orders/${id}`),
createShipment: (orderId, carrier, trackingNo, items) =>
createShipment: (orderId, carrier, trackingNo, items, shippingCompanyCode) =>
r("POST", `/shop/orders/${orderId}/shipments`, {
carrier,
tracking_no: trackingNo,
shipping_company_code: shippingCompanyCode ?? null,
items,
}),
listShipments: () => r("GET", "/shop/shipments"),
@@ -437,6 +456,10 @@ export function createApi(opts: ApiClientOptions): ApiClient {
confirmAftersaleReceipt: (id) => r("POST", `/shop/aftersales/${id}/confirm-receipt`),
refundAftersale: (id) => r("POST", `/shop/aftersales/${id}/refund`),
addAftersaleMessage: (id, body) => r("POST", `/shop/aftersales/${id}/messages`, body),
listFreightTemplates: () => r("GET", "/shop/freight-templates"),
createFreightTemplate: (body) => r("POST", "/shop/freight-templates", body),
updateFreightTemplate: (id, body) => r("PUT", `/shop/freight-templates/${id}`, body),
deleteFreightTemplate: (id) => r("DELETE", `/shop/freight-templates/${id}`),
},
admin: {
listUsers: (page = 1) => r("GET", "/admin/users", undefined, { page }),
+82
View File
@@ -68,6 +68,8 @@ export interface Product {
description: LocalizedText;
images: string[];
status: ProductStatus;
/** Freight template of the same shop; overrides the shop default. */
freight_template_id?: string | null;
created_at: string;
/** Units sold across paid orders; every product payload carries it. */
sold_count: number;
@@ -97,6 +99,8 @@ export interface Sku {
currency: string;
stock: number;
active: boolean;
/** Grams per unit for by-weight freight; null lines contribute zero weight. */
weight_grams?: number | null;
}
export interface Currency {
@@ -142,6 +146,9 @@ export interface OrderItem {
image: string | null;
/** Set when this line received flash-sale pricing. */
flash_sale_item_id: string | null;
/** Freight template + pricing method snapshotted at checkout. */
freight_template_id?: string | null;
freight_pricing_method?: FreightPricingMethod | null;
}
export interface Order {
@@ -156,6 +163,8 @@ export interface Order {
discount_minor: number;
/** Authoritative sum of completed refunds, integer minor units. */
refund_total_minor: number;
/** Server-computed shipping fee for this shop order. */
shipping_fee_minor: number;
/** The coupon this order redeemed, or null. */
coupon_id: string | null;
/** The group-buying activity this order joined, or null. */
@@ -473,6 +482,8 @@ export interface Shipment {
status: ShipmentStatus;
/** Not returned by the API; only shipment creation sends line items. */
items?: { order_item_id: string; qty: number }[];
/** Dictionary code of the company the merchant shipped with. */
shipping_company_code?: string | null;
shipped_at: string | null;
delivered_at: string | null;
created_at: string;
@@ -695,3 +706,74 @@ export interface AftersaleReturnTrackingBody {
}
export type AftersaleArbitration = "refund" | "reject";
// ---- freight / shipping ----
export type FreightPricingMethod = "by_piece" | "by_weight";
export interface FreightRegionRule {
id: string;
/** Region names/codes matched case-insensitively against the address region fields. */
regions: string[];
first_fee_minor: number;
first_unit: number;
additional_fee_minor: number;
additional_unit: number;
}
export interface FreightRegionRuleInput {
regions: string[];
first_fee_minor: number;
first_unit: number;
additional_fee_minor: number;
additional_unit: number;
}
export interface FreightTemplate {
id: string;
shop_id: string;
name: string;
is_default: boolean;
always_free: boolean;
pricing_method: FreightPricingMethod;
first_fee_minor: number;
/** Pieces for by_piece, grams for by_weight. */
first_unit: number;
additional_fee_minor: number;
additional_unit: number;
free_threshold_minor: number | null;
region_rules: FreightRegionRule[];
created_at: string;
updated_at: string;
}
export interface FreightTemplateInput {
name: string;
is_default?: boolean;
always_free?: boolean;
pricing_method: FreightPricingMethod;
first_fee_minor: number;
first_unit: number;
additional_fee_minor: number;
additional_unit: number;
free_threshold_minor?: number | null;
region_rules?: FreightRegionRuleInput[];
}
export interface ShippingCompany {
code: string;
name: LocalizedText;
active: boolean;
}
export interface ShippingQuoteShop {
shop_id: string;
fee_minor: number;
}
/** Server-computed per-shop shipping fees for the current cart and address. */
export interface ShippingQuote {
currency: string;
shops: ShippingQuoteShop[];
total_minor: number;
}