diff --git a/furniture-3d-system/prisma/migrations/20260810143000_add_jushuitan_fixed_virtual_inventory_tasks/migration.sql b/furniture-3d-system/prisma/migrations/20260810143000_add_jushuitan_fixed_virtual_inventory_tasks/migration.sql new file mode 100644 index 00000000..1d6ca19d --- /dev/null +++ b/furniture-3d-system/prisma/migrations/20260810143000_add_jushuitan_fixed_virtual_inventory_tasks/migration.sql @@ -0,0 +1,31 @@ +CREATE TABLE "IntakeJushuitanFixedVirtualInventoryTask" ( + "id" TEXT NOT NULL, + "productId" TEXT NOT NULL, + "productSpu" TEXT NOT NULL DEFAULT '', + "skuItemId" TEXT NOT NULL, + "skuCode" TEXT NOT NULL DEFAULT '', + "barcode" TEXT NOT NULL DEFAULT '', + "status" TEXT NOT NULL DEFAULT 'pending', + "scheduledAt" TIMESTAMP(3) NOT NULL, + "retryCount" INTEGER NOT NULL DEFAULT 0, + "lastAttemptAt" TIMESTAMP(3), + "pushedAt" TIMESTAMP(3), + "error" TEXT NOT NULL DEFAULT '', + "requestId" TEXT NOT NULL DEFAULT '', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "IntakeJushuitanFixedVirtualInventoryTask_pkey" PRIMARY KEY ("id"), + CONSTRAINT "IntakeJushuitanFixedVirtualInventoryTask_productId_fkey" + FOREIGN KEY ("productId") REFERENCES "IntakeNonIntegratedProduct"("id") + ON DELETE CASCADE ON UPDATE CASCADE +); + +CREATE UNIQUE INDEX "IntakeJushuitanFixedVirtualInventoryTask_productId_skuItemId_key" + ON "IntakeJushuitanFixedVirtualInventoryTask"("productId", "skuItemId"); + +CREATE INDEX "IntakeJushuitanFixedVirtualInventoryTask_status_scheduledAt_idx" + ON "IntakeJushuitanFixedVirtualInventoryTask"("status", "scheduledAt"); + +CREATE INDEX "IntakeJushuitanFixedVirtualInventoryTask_productId_idx" + ON "IntakeJushuitanFixedVirtualInventoryTask"("productId"); diff --git a/furniture-3d-system/prisma/schema.prisma b/furniture-3d-system/prisma/schema.prisma index b9e3a782..1f1c98ba 100644 --- a/furniture-3d-system/prisma/schema.prisma +++ b/furniture-3d-system/prisma/schema.prisma @@ -256,6 +256,7 @@ model IntakeNonIntegratedProduct { skuItems IntakeNonIntegratedSkuItem[] pricingRows IntakeNonIntegratedPricingRow[] productLimitRows IntakeNonIntegratedProductLimitRow[] + fixedVirtualInventoryTasks IntakeJushuitanFixedVirtualInventoryTask[] profile IntakeNonIntegratedProductProfile? modelAssets IntakeProductModelAsset[] @@ -671,6 +672,29 @@ model IntakeNonIntegratedPricingRow { @@index([systemEnteredAt, productId]) } +model IntakeJushuitanFixedVirtualInventoryTask { + id String @id @default(cuid()) + productId String + product IntakeNonIntegratedProduct @relation(fields: [productId], references: [id], onDelete: Cascade) + productSpu String @default("") + skuItemId String + skuCode String @default("") + barcode String @default("") + status String @default("pending") + scheduledAt DateTime + retryCount Int @default(0) + lastAttemptAt DateTime? + pushedAt DateTime? + error String @default("") + requestId String @default("") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + @@unique([productId, skuItemId]) + @@index([status, scheduledAt]) + @@index([productId]) +} + model IntakeNonIntegratedProductLimitRow { id String @id productId String diff --git a/furniture-3d-system/src/app/api/intake/non-integrated/[id]/jushuitan-product-pricing-push/route.ts b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/jushuitan-product-pricing-push/route.ts index 467253df..ee2a86cd 100644 --- a/furniture-3d-system/src/app/api/intake/non-integrated/[id]/jushuitan-product-pricing-push/route.ts +++ b/furniture-3d-system/src/app/api/intake/non-integrated/[id]/jushuitan-product-pricing-push/route.ts @@ -3,6 +3,7 @@ import { AppError, ErrorCode } from '@/server/core/errors' import { handleRequest, jsonError, jsonSuccess } from '@/server/core/response' import { intakeNonIntegratedProductService } from '@/server/services/intake-non-integrated-product.service' import { jushuitanProductPricingPushService } from '@/server/services/jushuitan-product-pricing-push.service' +import { jushuitanFixedVirtualInventoryService } from '@/server/services/jushuitan-fixed-virtual-inventory.service' import { OperationLogAction, OperationLogStatus, @@ -69,6 +70,11 @@ export async function POST(request: NextRequest, context: RouteContext) { product, targetSkuIds, }) + const fixedVirtualInventoryQueuedCount = + await jushuitanFixedVirtualInventoryService.enqueueForProduct( + product, + result.pushedSkuIds, + ) await operationLogService.record({ action: OperationLogAction.JUSHUITAN_PRODUCT_PUSH, @@ -84,10 +90,15 @@ export async function POST(request: NextRequest, context: RouteContext) { normalItemCount: result.normalItemCount, combineItemCount: result.combineItemCount, virtualInventoryPendingCount: result.virtualInventoryPendingCount, + fixedVirtualInventoryQueuedCount, }, }) - return jsonSuccess(result, 200, ctx.requestId) + return jsonSuccess( + { ...result, fixedVirtualInventoryQueuedCount }, + 200, + ctx.requestId, + ) } catch (error) { if (error instanceof AppError) { return jsonError(error.code, error.message, error.statusCode, ctx.requestId) diff --git a/furniture-3d-system/src/app/api/intake/non-integrated/jushuitan-virtual-inventory-push/route.ts b/furniture-3d-system/src/app/api/intake/non-integrated/jushuitan-virtual-inventory-push/route.ts index 655c33a7..4faefde8 100644 --- a/furniture-3d-system/src/app/api/intake/non-integrated/jushuitan-virtual-inventory-push/route.ts +++ b/furniture-3d-system/src/app/api/intake/non-integrated/jushuitan-virtual-inventory-push/route.ts @@ -14,6 +14,7 @@ import { applyVirtualInventoryRunFailureToRows, isDueVirtualInventoryRow, } from '@/server/services/jushuitan-virtual-inventory-queue.service' +import { jushuitanFixedVirtualInventoryService } from '@/server/services/jushuitan-fixed-virtual-inventory.service' function getCronUser(request: NextRequest): AuthUser | null { const cronSecret = process.env.JST_VIRTUAL_INVENTORY_CRON_SECRET?.trim() @@ -225,6 +226,15 @@ export async function POST(request: NextRequest) { } } + const fixedVirtualInventory = + await jushuitanFixedVirtualInventoryService.processDue({ + actor: user, + source: logSource, + actorId, + requestId: ctx.requestId, + now, + }) + return jsonSuccess( { processedProductCount: processedProducts.length, @@ -232,6 +242,7 @@ export async function POST(request: NextRequest) { skippedCount: processedProducts.reduce((sum, item) => sum + item.skipped, 0), failedCount: processedProducts.reduce((sum, item) => sum + item.failed, 0), processedProducts, + fixedVirtualInventory, }, 200, ctx.requestId, diff --git a/furniture-3d-system/src/server/repositories/jushuitan-fixed-virtual-inventory-task.repository.ts b/furniture-3d-system/src/server/repositories/jushuitan-fixed-virtual-inventory-task.repository.ts new file mode 100644 index 00000000..4bad84b6 --- /dev/null +++ b/furniture-3d-system/src/server/repositories/jushuitan-fixed-virtual-inventory-task.repository.ts @@ -0,0 +1,93 @@ +import type { IntakeJushuitanFixedVirtualInventoryTask } from "@prisma/client"; +import { prisma } from "@/lib/prisma"; + +export type CreateFixedVirtualInventoryTaskInput = { + productId: string; + productSpu: string; + skuItemId: string; + skuCode: string; + barcode: string; + scheduledAt: Date; +}; + +export type FixedVirtualInventoryRetryInput = { + id: string; + attemptedAt: Date; + scheduledAt: Date; + error: string; + requestId: string; +}; + +export type FixedVirtualInventorySuccessInput = { + id: string; + attemptedAt: Date; + requestId: string; +}; + +class JushuitanFixedVirtualInventoryTaskRepository { + async createMissing(tasks: CreateFixedVirtualInventoryTaskInput[]) { + if (tasks.length === 0) return 0; + + const result = + await prisma.intakeJushuitanFixedVirtualInventoryTask.createMany({ + data: tasks.map((task) => ({ + ...task, + status: "pending", + retryCount: 0, + error: "", + requestId: "", + })), + skipDuplicates: true, + }); + + return result.count; + } + + async findDue(now: Date, take = 100) { + return prisma.intakeJushuitanFixedVirtualInventoryTask.findMany({ + where: { + status: "pending", + scheduledAt: { lte: now }, + }, + orderBy: [{ scheduledAt: "asc" }, { createdAt: "asc" }], + take, + }); + } + + async markSucceeded(input: FixedVirtualInventorySuccessInput) { + return prisma.intakeJushuitanFixedVirtualInventoryTask.update({ + where: { id: input.id }, + data: { + status: "pushed", + pushedAt: input.attemptedAt, + lastAttemptAt: input.attemptedAt, + error: "", + requestId: input.requestId, + }, + }); + } + + async scheduleRetry(input: FixedVirtualInventoryRetryInput) { + return prisma.intakeJushuitanFixedVirtualInventoryTask.update({ + where: { id: input.id }, + data: { + status: "pending", + scheduledAt: input.scheduledAt, + retryCount: { increment: 1 }, + lastAttemptAt: input.attemptedAt, + error: input.error, + requestId: input.requestId, + }, + }); + } +} + +export type JushuitanFixedVirtualInventoryTaskRecord = + IntakeJushuitanFixedVirtualInventoryTask; +export type JushuitanFixedVirtualInventoryTaskRepositoryContract = Pick< + JushuitanFixedVirtualInventoryTaskRepository, + "createMissing" | "findDue" | "markSucceeded" | "scheduleRetry" +>; + +export const jushuitanFixedVirtualInventoryTaskRepository = + new JushuitanFixedVirtualInventoryTaskRepository(); diff --git a/furniture-3d-system/src/server/services/jushuitan-fixed-virtual-inventory.service.test.ts b/furniture-3d-system/src/server/services/jushuitan-fixed-virtual-inventory.service.test.ts new file mode 100644 index 00000000..1f74b6b6 --- /dev/null +++ b/furniture-3d-system/src/server/services/jushuitan-fixed-virtual-inventory.service.test.ts @@ -0,0 +1,166 @@ +import type { AuthUser } from "@/server/core/types"; +import type { + JushuitanFixedVirtualInventoryTaskRepositoryContract, + JushuitanFixedVirtualInventoryTaskRecord, +} from "@/server/repositories/jushuitan-fixed-virtual-inventory-task.repository"; +import { + OperationLogStatus, + operationLogService, +} from "./operation-log.service"; +import { + buildFixedVirtualInventoryPayload, + FIXED_VIRTUAL_INVENTORY_RETRY_DELAY_MS, + JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH, + JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY, + JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID, + JushuitanFixedVirtualInventoryService, +} from "./jushuitan-fixed-virtual-inventory.service"; + +function createTask( + overrides: Partial = {}, +): JushuitanFixedVirtualInventoryTaskRecord { + return { + id: "task-1", + productId: "product-1", + productSpu: "SPU-1", + skuItemId: "sku-1", + skuCode: "SKU-1", + barcode: "BARCODE-1", + status: "pending", + scheduledAt: new Date("2026-08-10T00:00:00.000Z"), + retryCount: 0, + lastAttemptAt: null, + pushedAt: null, + error: "", + requestId: "", + createdAt: new Date("2026-08-09T00:00:00.000Z"), + updatedAt: new Date("2026-08-09T00:00:00.000Z"), + ...overrides, + }; +} + +function createRepository(tasks: JushuitanFixedVirtualInventoryTaskRecord[]) { + return { + createMissing: jest.fn(async () => 1), + findDue: jest.fn(async () => tasks), + markSucceeded: jest.fn(async () => undefined), + scheduleRetry: jest.fn(async () => undefined), + } satisfies JushuitanFixedVirtualInventoryTaskRepositoryContract; +} + +const actor = { + id: "user-1", + email: "user@example.test", + name: "Test User", + role: "SUPER_ADMIN", + status: "ACTIVE", + businessRoles: [], +} as AuthUser; + +describe("Jushuitan fixed virtual inventory service", () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + it("builds the fixed warehouse and quantity payload from a barcode", () => { + expect(buildFixedVirtualInventoryPayload("BARCODE-1")).toEqual({ + wms_co_id: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID, + items: [ + { sku_id: "BARCODE-1", qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY }, + ], + }); + }); + + it("enqueues only the selected sku items", async () => { + const repository = createRepository([]); + const service = new JushuitanFixedVirtualInventoryService(repository); + + await service.enqueueForProduct( + { + id: "product-1", + spu: "SPU-1", + skuItems: [ + { id: "sku-1", skuCode: "SKU-1", barcode: "BARCODE-1" } as never, + { id: "sku-2", skuCode: "SKU-2", barcode: "BARCODE-2" } as never, + ], + }, + ["sku-2"], + ); + + expect(repository.createMissing).toHaveBeenCalledWith([ + expect.objectContaining({ + productId: "product-1", + skuItemId: "sku-2", + barcode: "BARCODE-2", + }), + ]); + }); + + it("pushes a due task and records the external request id", async () => { + const repository = createRepository([createTask()]); + const post = jest.fn(async () => ({ + path: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH, + code: 0, + message: "ok", + raw: { code: 0, data: { request_id: "jst-request-1" } }, + })); + const record = jest + .spyOn(operationLogService, "record") + .mockResolvedValue(undefined); + const service = new JushuitanFixedVirtualInventoryService(repository, post); + + const result = await service.processDue({ + actor, + source: "SYSTEM", + requestId: "local-request-1", + now: new Date("2026-08-10T00:00:00.000Z"), + }); + + expect(result).toEqual({ processed: 1, pushed: 1, failed: 0 }); + expect(post).toHaveBeenCalledWith( + JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH, + buildFixedVirtualInventoryPayload("BARCODE-1"), + ); + expect(repository.markSucceeded).toHaveBeenCalledWith({ + id: "task-1", + attemptedAt: new Date("2026-08-10T00:00:00.000Z"), + requestId: "jst-request-1", + }); + expect(record).toHaveBeenCalledWith( + expect.objectContaining({ status: OperationLogStatus.SUCCESS }), + ); + }); + + it("schedules every failed task exactly two hours later", async () => { + const repository = createRepository([createTask()]); + const post = jest.fn(async () => { + throw new Error("Jushuitan rejected the request"); + }); + const record = jest + .spyOn(operationLogService, "record") + .mockResolvedValue(undefined); + const service = new JushuitanFixedVirtualInventoryService(repository, post); + const now = new Date("2026-08-10T00:00:00.000Z"); + + const result = await service.processDue({ + actor, + source: "SYSTEM", + requestId: "local-request-1", + now, + }); + + expect(result).toEqual({ processed: 1, pushed: 0, failed: 1 }); + expect(repository.scheduleRetry).toHaveBeenCalledWith({ + id: "task-1", + attemptedAt: now, + scheduledAt: new Date( + now.getTime() + FIXED_VIRTUAL_INVENTORY_RETRY_DELAY_MS, + ), + error: "Jushuitan rejected the request", + requestId: "local-request-1", + }); + expect(record).toHaveBeenCalledWith( + expect.objectContaining({ status: OperationLogStatus.FAILED }), + ); + }); +}); diff --git a/furniture-3d-system/src/server/services/jushuitan-fixed-virtual-inventory.service.ts b/furniture-3d-system/src/server/services/jushuitan-fixed-virtual-inventory.service.ts new file mode 100644 index 00000000..4a7a4790 --- /dev/null +++ b/furniture-3d-system/src/server/services/jushuitan-fixed-virtual-inventory.service.ts @@ -0,0 +1,230 @@ +import type { AuthUser } from "@/server/core/types"; +import type { NonIntegratedSkuItem } from "@/app/intake/non-integrated/nonIntegratedStorage"; +import { + postJushuitanWithDefaultCredentials, + type JushuitanApiResult, +} from "./jushuitan-product-pricing-push.service"; +import { + OperationLogAction, + OperationLogStatus, + operationLogService, +} from "./operation-log.service"; +import { + jushuitanFixedVirtualInventoryTaskRepository, + type JushuitanFixedVirtualInventoryTaskRepositoryContract, + type JushuitanFixedVirtualInventoryTaskRecord, +} from "@/server/repositories/jushuitan-fixed-virtual-inventory-task.repository"; + +export const JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH = + "/open/webapi/itemapi/iteminventory/batchupdatewmsvirtualqtys"; +export const JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID = 15994660; +export const JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY = 100000; +export const FIXED_VIRTUAL_INVENTORY_RETRY_DELAY_MS = 2 * 60 * 60 * 1000; + +type FixedVirtualInventoryProduct = { + id: string; + spu: string; + skuItems: NonIntegratedSkuItem[]; +}; + +type FixedVirtualInventoryRunInput = { + actor: AuthUser; + source: "USER" | "SYSTEM"; + actorId?: string; + requestId: string; + now?: Date; +}; + +type FixedVirtualInventoryApi = ( + path: string, + biz: Record, +) => Promise; + +function readRequestId(raw: unknown) { + if (!raw || typeof raw !== "object") return ""; + const record = raw as Record; + const data = record.data; + const dataRecord = + data && typeof data === "object" ? (data as Record) : null; + + for (const value of [ + record.request_id, + record.requestId, + dataRecord?.request_id, + dataRecord?.requestId, + ]) { + if (typeof value === "string" && value.trim()) return value.trim(); + } + + return ""; +} + +function normalizeError(error: unknown) { + return error instanceof Error + ? error.message + : "Jushuitan fixed virtual inventory push failed"; +} + +function buildFixedVirtualInventoryPayload(barcode: string) { + return { + wms_co_id: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID, + items: [ + { + sku_id: barcode, + qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY, + }, + ], + }; +} + +class JushuitanFixedVirtualInventoryService { + constructor( + private readonly repository: JushuitanFixedVirtualInventoryTaskRepositoryContract = jushuitanFixedVirtualInventoryTaskRepository, + private readonly post: FixedVirtualInventoryApi = postJushuitanWithDefaultCredentials, + ) {} + + async enqueueForProduct( + product: FixedVirtualInventoryProduct, + targetSkuIds: string[], + now = new Date(), + ) { + const targetSkuIdSet = new Set(targetSkuIds); + const tasks = product.skuItems + .filter((sku) => targetSkuIdSet.has(sku.id)) + .map((sku) => ({ + productId: product.id, + productSpu: product.spu, + skuItemId: sku.id, + skuCode: sku.skuCode, + barcode: sku.barcode, + scheduledAt: now, + })); + + return this.repository.createMissing(tasks); + } + + async processDue(input: FixedVirtualInventoryRunInput) { + const now = input.now ?? new Date(); + const tasks = await this.repository.findDue(now); + const result = { processed: 0, pushed: 0, failed: 0 }; + + for (const task of tasks) { + result.processed += 1; + if (await this.processOne(task, input)) { + result.pushed += 1; + } else { + result.failed += 1; + } + } + + return result; + } + + private async processOne( + task: JushuitanFixedVirtualInventoryTaskRecord, + input: FixedVirtualInventoryRunInput, + ) { + const attemptedAt = input.now ?? new Date(); + const barcode = task.barcode.trim(); + if (!barcode) { + return this.scheduleRetry( + task, + input, + attemptedAt, + "SKU barcode is empty", + ); + } + const payload = buildFixedVirtualInventoryPayload(barcode); + + try { + const response = await this.post( + JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH, + payload, + ); + const requestId = readRequestId(response.raw) || input.requestId; + await this.repository.markSucceeded({ + id: task.id, + attemptedAt, + requestId, + }); + await operationLogService.record({ + action: OperationLogAction.JUSHUITAN_VIRTUAL_INVENTORY_PUSH, + status: OperationLogStatus.SUCCESS, + source: input.source, + actorId: input.actorId, + actorName: input.actor.name, + productId: task.productId, + productSpu: task.productSpu, + skuId: task.skuItemId, + skuCode: task.skuCode, + message: "Jushuitan fixed virtual inventory push succeeded", + requestId, + details: { + flow: "fixed-wms-virtual-inventory", + path: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH, + wmsCoId: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID, + qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY, + barcode: task.barcode, + }, + }); + return true; + } catch (error) { + return this.scheduleRetry( + task, + input, + attemptedAt, + normalizeError(error), + ); + } + } + + private async scheduleRetry( + task: JushuitanFixedVirtualInventoryTaskRecord, + input: FixedVirtualInventoryRunInput, + attemptedAt: Date, + reason: string, + ) { + const scheduledAt = new Date( + attemptedAt.getTime() + FIXED_VIRTUAL_INVENTORY_RETRY_DELAY_MS, + ); + await this.repository.scheduleRetry({ + id: task.id, + attemptedAt, + scheduledAt, + error: reason, + requestId: input.requestId, + }); + await operationLogService.record({ + action: OperationLogAction.JUSHUITAN_VIRTUAL_INVENTORY_PUSH, + status: OperationLogStatus.FAILED, + source: input.source, + actorId: input.actorId, + actorName: input.actor.name, + productId: task.productId, + productSpu: task.productSpu, + skuId: task.skuItemId, + skuCode: task.skuCode, + errorCode: "JUSHUITAN_FIXED_VIRTUAL_INVENTORY_FAILED", + message: `Jushuitan fixed virtual inventory push failed; retry at ${scheduledAt.toISOString()}: ${reason}`, + requestId: input.requestId, + details: { + flow: "fixed-wms-virtual-inventory", + path: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_PATH, + wmsCoId: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_WMS_CO_ID, + qty: JUSHUITAN_FIXED_VIRTUAL_INVENTORY_QTY, + barcode: task.barcode, + scheduledAt: scheduledAt.toISOString(), + reason, + }, + }); + return false; + } +} + +export const jushuitanFixedVirtualInventoryService = + new JushuitanFixedVirtualInventoryService(); + +export { + JushuitanFixedVirtualInventoryService, + buildFixedVirtualInventoryPayload, +}; diff --git a/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.ts b/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.ts index 1c8dc9bf..b5db15b2 100644 --- a/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.ts +++ b/furniture-3d-system/src/server/services/jushuitan-product-pricing-push.service.ts @@ -50,7 +50,7 @@ export type JushuitanVirtualInventoryPushInput = { targetSkuIds: string[] } -type JushuitanApiResult = { +export type JushuitanApiResult = { path: string code: string | number | null message: string @@ -625,6 +625,13 @@ async function postJushuitan( } } +export async function postJushuitanWithDefaultCredentials( + path: string, + biz: Record, +) { + return postJushuitan(readJushuitanCredentials(), path, biz) +} + function requireText(value: string, label: string) { const normalizedValue = value.trim() if (!normalizedValue) {