diff --git a/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java b/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java index ccf424a03..46230f310 100644 --- a/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java +++ b/iam/src/test/java/com/baiyue/iam/repository/FinanceG1G2PermissionSqlContractTest.java @@ -46,7 +46,8 @@ class FinanceG1G2PermissionSqlContractTest { @Test void unifiedSqlMirrorsCatalogAndGrantsTheFinanceCatalogToPlatformAdmin() throws Exception { - String sql = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8); + String sql = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8) + .replace("\r\n", "\n"); String financeSection = sql.substring(sql.indexOf("-- Finance F1 人工财务权限"), sql.indexOf("-- Admin 原型菜单与财务五页权限补充")); diff --git a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java index 24e2dadb3..25a95c586 100644 --- a/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java +++ b/order/src/main/java/com/baiyue/order/service/UnifiedOrderQueryService.java @@ -297,8 +297,11 @@ public class UnifiedOrderQueryService { order.getCreatedAt(), order.getUpdatedAt(), order.getRequestedPickupAt()); } FirstlegUnifiedOrderClient.Inbound inbound = summary.inbound(); + boolean pendingReview = order.getStatus() == OrderStatus.SUBMITTED; + String mainStatus = pendingReview ? "PENDING_REVIEW" : summary.mainStatus(); + String nextAction = pendingReview ? "APPROVE_ORDER" : summary.nextAction(); return new UnifiedOrderResponse.Row(order.getId(), order.getTenantId(), order.getOrderNo(), - summary.mainStatus(), summary.nextAction(), relation(order, childCounts), order.getStatus(), + mainStatus, nextAction, relation(order, childCounts), order.getStatus(), order.getCustomerId(), order.getCustomerName(), order.getChannelCode(), order.getChannelName(), Boolean.TRUE.equals(order.getChannelChanged()), order.getReceivingWarehouseId(), order.getReceivingWarehouseName(), order.getDestinationWarehouseId(), order.getDestinationWarehouseName(), inbound.id(), inbound.version(), inbound.inboundCode(), diff --git a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java index 1df73770b..f1d3c1de6 100644 --- a/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java +++ b/order/src/test/java/com/baiyue/order/service/UnifiedOrderQueryServiceTest.java @@ -81,6 +81,26 @@ class UnifiedOrderQueryServiceTest { verify(firstleg, never()).summaries(any(), any()); } + @Test + void submittedOrderRemainsPendingReviewWhenPreparingInboundAlreadyExists() { + UnifiedOrderQuery query = query(null); + OrderEntity order = order(OrderStatus.SUBMITTED); + var summary = summary(null, null); + when(summary.inbound().processStatus()).thenReturn("PREPARING"); + when(summary.inbound().orderStatusSnapshot()).thenReturn("SUBMITTED"); + when(orders.unifiedPage(eq(10001L), eq(query), any())) + .thenReturn(new PageResult<>(List.of(order), 1, 20, 1, 1)); + when(firstleg.summaries(eq("internal-token"), any())) + .thenReturn(ApiResponse.success(List.of(summary))); + + assertThat(service.page(10001L, query).records()).singleElement().satisfies(row -> { + assertThat(row.mainStatus()).isEqualTo("PENDING_REVIEW"); + assertThat(row.nextAction()).isEqualTo("APPROVE_ORDER"); + assertThat(row.inboundId()).isEqualTo(88L); + assertThat(row.internalOrderStatus()).isEqualTo(OrderStatus.SUBMITTED); + }); + } + @Test void newlyCreatedOrderUsesItsOwnCargoPackageCountBeforeInboundExists() { UnifiedOrderQuery query = query(null); diff --git a/scripts/admin-test-env-acceptance.mjs b/scripts/admin-test-env-acceptance.mjs index 3739f0d36..a72566c04 100644 --- a/scripts/admin-test-env-acceptance.mjs +++ b/scripts/admin-test-env-acceptance.mjs @@ -521,9 +521,12 @@ export class AcceptanceRun { const headers = { Accept: 'application/json', ...extraHeaders } if (token) headers.Authorization = `Bearer ${token}` if (body !== undefined) headers['Content-Type'] = 'application/json; charset=utf-8' + const requestBody = path === '/api/auth/login' && body?.username && !body?.loginId + ? { ...body, loginId: body.username, username: undefined } + : body try { const response = await fetch(`${this.options.gateway}${path}`, { - method, headers, body: body === undefined ? undefined : JSON.stringify(body), + method, headers, body: requestBody === undefined ? undefined : JSON.stringify(requestBody), signal: AbortSignal.timeout(30_000), }) const content = await response.text() diff --git a/scripts/admin-test-env-modules/orders.mjs b/scripts/admin-test-env-modules/orders.mjs index 6f6404e3d..7e66a8023 100644 --- a/scripts/admin-test-env-modules/orders.mjs +++ b/scripts/admin-test-env-modules/orders.mjs @@ -25,6 +25,23 @@ function rotate(values, offset) { return [...values.slice(shift), ...values.slice(0, shift)] } function key(context, operation) { return `${context.marker}-${operation}`.slice(0, 128) } +function declarationCargo(source, index) { + const number = (value) => Number.isFinite(Number(value)) ? Number(value) : 0 + const optionalNumber = (value) => value === null || value === undefined || value === '' ? null : number(value) + return { + itemNo: number(source?.itemNo) || index + 1, + cargoName: String(source?.cargoName || ''), cargoNameEn: source?.cargoNameEn || null, + skuNo: source?.skuNo || null, model: source?.model || null, + packageCount: number(source?.packageCount), totalQuantity: number(source?.totalQuantity), + lengthCm: number(source?.lengthCm), widthCm: number(source?.widthCm), heightCm: number(source?.heightCm), + grossWeightKg: number(source?.grossWeightKg), volumeCbm: number(source?.volumeCbm), + netWeightKg: optionalNumber(source?.netWeightKg), hsCode: source?.hsCode || null, + customsHsCode: source?.customsHsCode || null, unitsPerPackage: optionalNumber(source?.unitsPerPackage), + unitDeclaredValue: optionalNumber(source?.unitDeclaredValue), declaredValue: optionalNumber(source?.declaredValue), + currency: source?.currency || null, material: source?.material || null, usage: source?.usage || null, + brand: source?.brand || null, cargoRemark: source?.cargoRemark || source?.remark || null, + } +} export function orderPersistenceFacts(detail) { return JSON.stringify({ order: detail?.order ?? null, @@ -353,7 +370,7 @@ function buildPayload(context, profile, master, productImageFileId) { const selectedBoxes = BOXES.slice(0, skuCount) const orderProfile = context.context?.orderProfile || {} const isPrivate = master.destinationType === 'PRIVATE' - const pickupMethod = orderProfile.pickupMethod || 'PICKUP' + const pickupMethod = orderProfile.pickupMethod || 'SELF_EXPRESS' const receiver = isPrivate ? { companyName: `${companyName} 私人收件人`, contactName: `收件人-${context.marker}`.slice(0, 64), phone: '12025550188', email: 'private-receiver@example.com', countryCode: 'US', @@ -405,7 +422,7 @@ function buildPayload(context, profile, master, productImageFileId) { shipmentId: master.destinationType === 'FBA' ? shipmentId : null, referenceId: master.destinationType === 'FBA' ? referenceId : null, upcNo: `${Date.now()}${index}`.slice(-12), - productImageFileId, dangerousGoods: false, + productImageFileId, dangerousGoods: false, woodenProduct: false, cargoRemark: `${context.marker} boxType=${box.boxType}`, } }), @@ -665,9 +682,50 @@ export async function run(context) { pageOk(inboundPage) && records(inboundPage).some((row) => id(row.id) === inboundId), `inboundId=${inboundId}, total=${inboundPage?.total ?? 'n/a'}`) + const editableDetailResponse = await context.request('GET', firstLegDetailPath) + const editableDetail = context.apiSuccess(MODULE, 'Admin 读取审核后客户申报资料', editableDetailResponse) + const declarationVersion = Number(editableDetail?.order?.version) + const declarationRemark = `${context.marker} Admin 修改客户申报资料` + const declarationPayload = { + expectedVersion: declarationVersion, + customerRemark: declarationRemark, + expectedArrivalAt: editableDetail?.order?.expectedArrivalAt || editableDetail?.summary?.expectedArrivalAt || null, + channelCode: editableDetail?.order?.channelCode || editableDetail?.summary?.channelCode, + channelName: editableDetail?.order?.channelName || editableDetail?.summary?.channelName, + cargoes: (editableDetail?.cargoes || []).map(declarationCargo), + } + const declarationUpdateResponse = await context.request('PUT', + `/api/platform/v1/orders/${encodeURIComponent(orderId)}/declaration?tenantId=${encodeURIComponent(tenantId)}`, + declarationPayload) + const declarationUpdate = context.apiSuccess(MODULE, 'Admin 修改审核后客户申报资料', declarationUpdateResponse) + context.assert(MODULE, '申报修改保持订单号入仓码和渠道且版本递增', declarationUpdateResponse, + Number.isFinite(declarationVersion) && Number(declarationUpdate?.version) === declarationVersion + 1 + && id(declarationUpdate?.inboundCode) === id(inbound.summary.inboundCode) + && declarationUpdate?.channelChanged === false, + `version=${declarationVersion}->${declarationUpdate?.version ?? 'missing'}, inboundCode=${declarationUpdate?.inboundCode ?? 'missing'}`, + CONTRACT_OPTIONS) + + const staleDeclarationResponse = await context.request('PUT', + `/api/platform/v1/orders/${encodeURIComponent(orderId)}/declaration?tenantId=${encodeURIComponent(tenantId)}`, + { ...declarationPayload, customerRemark: `${declarationRemark} stale` }) + context.assert(MODULE, '申报资料旧版本并发修改返回A0409', staleDeclarationResponse, + staleDeclarationResponse.http === 200 && String(staleDeclarationResponse.body?.code) === 'A0409', + `code=${staleDeclarationResponse.body?.code ?? 'missing'}`, CONTRACT_OPTIONS) + + let declarationConverged = false + let convergedInboundResponse = inboundResponse + for (let attempt = 0; attempt < 30; attempt += 1) { + convergedInboundResponse = await context.request('GET', inboundPath) + const currentInbound = success(convergedInboundResponse) ? convergedInboundResponse.body?.data : null + if (currentInbound?.publicRemark === declarationRemark) { declarationConverged = true; break } + if (attempt < 29) await sleep(1_000) + } + context.assert(MODULE, '申报修改异步同步到Firstleg入库事实', convergedInboundResponse, + declarationConverged, `expectedRemark=${declarationRemark}`, CONTRACT_OPTIONS) + context.createdResources.orders.inboundId = inboundId context.createdResources.orders.inboundCode = inbound.summary.inboundCode - context.createdResources.orders.customerRemark = payload.customerRemark + context.createdResources.orders.customerRemark = declarationRemark context.createdResources.orders.internalRemark = `${context.marker} Admin 订单链路验收` context.createdResources.orders.cargoRemark = payload.cargoes?.[0]?.cargoRemark || null context.createdResources.orders.processStatus = inbound.summary.processStatus diff --git a/scripts/client-test-env-acceptance.mjs b/scripts/client-test-env-acceptance.mjs index a996d8588..afd5308a7 100644 --- a/scripts/client-test-env-acceptance.mjs +++ b/scripts/client-test-env-acceptance.mjs @@ -42,6 +42,7 @@ if (!Number.isFinite(timeoutMs) || timeoutMs < 1_000) { const now = new Date() const marker = `${compactTimestamp(now)}-${randomUUID().slice(0, 8)}` const username = `client_${compactTimestamp(now)}_${randomUUID().slice(0, 6)}`.slice(0, 64) +const temporaryUsername = `renamed_${compactTimestamp(now)}_${randomUUID().slice(0, 6)}`.slice(0, 64) const clientPassword = `Client${randomUUID().replaceAll('-', '').slice(0, 14)}9!` const phoneSuffix = `${Date.now()}`.slice(-8) const legalPhone = `139${phoneSuffix}` @@ -91,6 +92,7 @@ try { await qualificationAndReviewModule() await merchantAfterApprovalModule() await profileManagementModule() + await loginIdentifierModule() await rbacAndMemberModule() await orderLifecycleModule() await isolationModule() @@ -117,7 +119,7 @@ async function registrationModule() { displayName: `测试管理员-${marker}`.slice(0, 64), companyName, legalPerson: '测试法人', - legalPhone, + phone: legalPhone, contactEmail: `${username}@example.com`, countryCode: 'CN', address: '广东省深圳市南山区测试路 1 号', @@ -213,7 +215,6 @@ async function qualificationAndReviewModule() { const png = testPng() const form = new FormData() form.append('businessLicense', new Blob([png], { type: 'image/png' }), `business-license-${marker}.png`) - form.append('legalPersonId', new Blob([png], { type: 'image/png' }), `legal-person-id-${marker}.png`) await apiStep('qualification-review', 'upload required qualification package', { method: 'POST', @@ -223,7 +224,7 @@ async function qualificationAndReviewModule() { }, ({ json }) => { requireSuccess(json) assertPresent(json.data?.businessLicenseQualificationId, 'business license qualification id') - assertPresent(json.data?.legalPersonIdQualificationId, 'legal person qualification id') + assertTrue(json.data?.legalPersonIdQualificationId == null, 'legal person qualification id must stay optional') assertEqual(json.data?.tenantStatus, 'PROFILE_REQUIRED', 'tenant status after upload') }, ({ json }) => ({ businessLicenseQualificationId: json.data.businessLicenseQualificationId, @@ -274,7 +275,9 @@ async function qualificationAndReviewModule() { method: 'GET', path: '/api/merchant/v1/iam/tenant/qualifications', token: merchantToken, }, ({ json }) => { requireSuccess(json) - assertQualificationStatuses(json.data, ['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'], 'PENDING') + assertQualificationStatuses(json.data, ['BUSINESS_LICENSE'], 'PENDING') + assertTrue(!arrayData(json.data).some((item) => item.qualificationType === 'LEGAL_PERSON_ID'), + 'optional legal person ID must remain absent') assertQualificationStatuses(json.data, ['FREIGHT_FORWARDER_CERTIFICATE', 'BANK_ACCOUNT_LICENSE'], 'PENDING') }, ({ json }) => ({ qualifications: qualificationEvidence(json.data) })) @@ -293,6 +296,19 @@ async function qualificationAndReviewModule() { }, () => ({ username: platformUsername })) platformToken = platformLogin.json.data.accessToken + const dimensionPath = `/api/platform/v1/iam/tenants/${report.resources.tenantId}/dimension-initialization` + for (let attempt = 0; attempt < 30; attempt += 1) { + const statusResponse = await httpRequest({ method: 'GET', path: dimensionPath, token: platformToken }) + if (String(statusResponse.json?.data?.status) === 'READY') break + if (attempt < 29) await new Promise((resolve) => setTimeout(resolve, 1_000)) + } + await apiStep('qualification-review', 'customer default dimensions are ready before approval', { + method: 'GET', path: dimensionPath, token: platformToken, + }, ({ json }) => { + requireSuccess(json) + assertEqual(json.data?.status, 'READY', 'dimension initialization status') + }, ({ json }) => ({ status: json.data.status, attemptCount: json.data.attemptCount })) + const reviews = await apiStep('qualification-review', 'platform finds pending review', { method: 'GET', path: '/api/platform/v1/iam/reviews', token: platformToken, }, ({ json }) => { @@ -310,7 +326,9 @@ async function qualificationAndReviewModule() { method: 'GET', path: `/api/platform/v1/iam/tenants/${report.resources.tenantId}/qualifications`, token: platformToken, }, ({ json }) => { requireSuccess(json) - assertQualificationStatuses(json.data, ['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'], 'PENDING') + assertQualificationStatuses(json.data, ['BUSINESS_LICENSE'], 'PENDING') + assertTrue(!arrayData(json.data).some((item) => item.qualificationType === 'LEGAL_PERSON_ID'), + 'platform must not require a legal person ID record') }, ({ json }) => ({ qualifications: qualificationEvidence(json.data) })) const approval = await apiStep('qualification-review', 'platform approves enterprise', { @@ -343,6 +361,51 @@ async function qualificationAndReviewModule() { }) } +async function loginIdentifierModule() { + const staleToken = merchantToken + await apiStep('login-identifiers', 'change username with current-password verification', { + method: 'PUT', path: '/api/merchant/v1/iam/profile/login-identifiers', token: merchantToken, + body: { username: temporaryUsername, phone: legalPhone, currentPassword: clientPassword }, + }, ({ json }) => { + requireSuccess(json) + assertEqual(json.data?.username, temporaryUsername, 'updated username') + assertEqual(json.data?.phone, legalPhone, 'updated phone') + }, ({ json }) => ({ username: json.data.username, phone: json.data.phone })) + + await apiStep('login-identifiers', 'identifier change revokes the previous access token', { + method: 'GET', path: '/api/merchant/v1/iam/profile', token: staleToken, + }, ({ status, json }) => { + assertEqual(status, 401, 'HTTP status') + assertTrue(String(json?.code || '').length > 0, 'authentication error code must be present') + }, ({ status, json }) => ({ status, rejectionCode: json?.code })) + + const renamedLogin = await apiStep('login-identifiers', 'login with the changed username', { + method: 'POST', path: '/api/auth/login', body: { username: temporaryUsername, password: clientPassword }, + }, ({ json }) => { + requireSuccess(json) + assertPresent(json.data?.accessToken, 'accessToken') + }, () => ({ username: temporaryUsername })) + merchantToken = renamedLogin.json.data.accessToken + merchantRefreshToken = renamedLogin.json.data.refreshToken + + await apiStep('login-identifiers', 'restore the original username for the remaining acceptance flow', { + method: 'PUT', path: '/api/merchant/v1/iam/profile/login-identifiers', token: merchantToken, + body: { username, phone: legalPhone, currentPassword: clientPassword }, + }, ({ json }) => { + requireSuccess(json) + assertEqual(json.data?.username, username, 'restored username') + }, ({ json }) => ({ username: json.data.username })) + + const restoredLogin = await apiStep('login-identifiers', 'login after restoring the original username', { + method: 'POST', path: '/api/auth/login', body: { username, password: clientPassword }, + }, ({ json }) => { + requireSuccess(json) + assertPresent(json.data?.accessToken, 'accessToken') + }, () => ({ username })) + merchantToken = restoredLogin.json.data.accessToken + merchantRefreshToken = restoredLogin.json.data.refreshToken +} + async function merchantAfterApprovalModule() { const login = await apiStep('merchant-session', 'client re-login after approval', { method: 'POST', path: '/api/auth/login', body: { username, password: clientPassword }, @@ -365,7 +428,9 @@ async function merchantAfterApprovalModule() { method: 'GET', path: '/api/merchant/v1/iam/tenant/qualifications', token: merchantToken, }, ({ json }) => { requireSuccess(json) - assertQualificationStatuses(json.data, ['BUSINESS_LICENSE', 'LEGAL_PERSON_ID'], 'APPROVED') + assertQualificationStatuses(json.data, ['BUSINESS_LICENSE'], 'APPROVED') + assertTrue(!arrayData(json.data).some((item) => item.qualificationType === 'LEGAL_PERSON_ID'), + 'approved enterprise must not gain a synthetic legal person ID record') assertQualificationStatuses(json.data, ['FREIGHT_FORWARDER_CERTIFICATE', 'BANK_ACCOUNT_LICENSE'], 'APPROVED') }, ({ json }) => ({ qualifications: qualificationEvidence(json.data) })) @@ -1004,9 +1069,9 @@ async function orderLifecycleModule() { }, ({ bytes, headers }) => ({ sizeBytes: bytes.length, contentType: headers.get('content-type') })) const payload = buildOrderPayload(masterData, productImageFileId, 'ACTIVE', 'fresh draft after approval') - const invalidOrderCases = [ + const incompleteSubmitCases = [ { - name: 'FBA order missing Amazon identifiers is rejected', + name: 'FBA order missing Amazon identifiers', mutate(value) { value.amazonWarehouseCode = null value.shipmentId = null @@ -1014,7 +1079,7 @@ async function orderLifecycleModule() { }, }, { - name: 'FBA cargo missing SKU UPC shipment and reference is rejected', + name: 'FBA cargo missing SKU UPC shipment and reference', mutate(value) { value.cargoes[0].skuNo = null value.cargoes[0].upcNo = null @@ -1022,9 +1087,35 @@ async function orderLifecycleModule() { value.cargoes[0].referenceId = null }, }, + ] + for (const testCase of incompleteSubmitCases) { + const incompletePayload = structuredClone(payload) + testCase.mutate(incompletePayload) + const draft = await apiStep('order-validation', `${testCase.name} can be saved as a draft`, { + method: 'POST', path: '/api/merchant/v1/orders/first-leg', token: merchantToken, + headers: { 'Idempotency-Key': newIdempotencyKey('incomplete-draft') }, body: incompletePayload, + }, ({ json }) => { + requireSuccess(json) + assertPresent(json.data, 'incomplete draft order id') + }, ({ json }) => ({ orderId: json.data, status: 'DRAFT' })) + report.resources.orderIds.push(draft.json.data) + await softApiStep('order-validation', `${testCase.name} is rejected on submit`, { + method: 'POST', path: `/api/merchant/v1/orders/first-leg/${draft.json.data}/submit`, token: merchantToken, + headers: { 'Idempotency-Key': newIdempotencyKey('incomplete-submit') }, + }, ({ json, status }) => { + assertEqual(status, 200, 'HTTP status') + assertEqual(String(json?.code), 'A0422', 'business code') + }, ({ json }) => ({ orderId: draft.json.data, rejectionCode: json?.code, message: json?.message })) + } + const invalidOrderCases = [ { - name: 'PICKUP order without sender is rejected', - mutate(value) { value.sender = null }, + name: 'PICKUP order without a selected pickup address is rejected', + mutate(value) { + value.pickupMethod = 'PICKUP' + value.requestedPickupAt = new Date(Date.now() + 7 * 86_400_000).toISOString().slice(0, 19) + value.sender = null + value.senderAddressId = null + }, }, { name: 'PRIVATE destination without receiver is rejected', @@ -1191,11 +1282,11 @@ async function orderLifecycleModule() { requireSuccess(json) assertPresent(pageRecords(json.data).find((item) => String(item.orderId) === String(orderId)), 'combined-filtered order') }, ({ json }) => ({ total: json.data?.total, orderId })) - await apiStep('order-filters', 'sender phone filter contains submitted order', { - method: 'GET', path: `/api/merchant/v1/orders/first-leg?page=1&size=50&customerContact=${encodeURIComponent(legalPhone)}`, token: merchantToken, + await apiStep('order-filters', 'customer contact filter contains submitted order by customer name', { + method: 'GET', path: `/api/merchant/v1/orders/first-leg?page=1&size=50&customerContact=${encodeURIComponent(payload.customerName)}`, token: merchantToken, }, ({ json }) => { requireSuccess(json) - assertPresent(pageRecords(json.data).find((item) => String(item.orderId) === String(orderId)), 'sender-phone-filtered order') + assertPresent(pageRecords(json.data).find((item) => String(item.orderId) === String(orderId)), 'customer-contact-filtered order') }, ({ json }) => ({ total: json.data?.total, orderId })) await apiStep('order-filters', 'unmatched keyword returns an empty order page', { method: 'GET', path: `/api/merchant/v1/orders/first-leg?page=1&size=50&keyword=${randomUUID()}`, token: merchantToken, @@ -1815,7 +1906,7 @@ function buildOrderPayload(masterData, productImageFileId, suffix, remark) { destinationWarehouseName: masterData.destination.warehouseName, expectedShipDate: addDays(new Date(), 30), customerRemark: `${remark} ${marker}`, - pickupMethod: 'PICKUP', + pickupMethod: 'SELF_EXPRESS', receivingWarehouseId: masterData.receiving.id, receivingWarehouseName: masterData.receiving.warehouseName, channelCode: masterData.channel.channelCode, @@ -1867,6 +1958,7 @@ function buildOrderPayload(masterData, productImageFileId, suffix, remark) { upcNo: `123456${`${Date.now()}`.slice(-6)}`.slice(0, 12), productImageFileId, dangerousGoods: false, + woodenProduct: false, cargoRemark: `cargo-${remark} ${marker}`, }], } @@ -1911,10 +2003,13 @@ async function softApiStep(module, name, request, validate, evidenceBuilder = de async function httpRequest({ method = 'GET', path, token, headers = {}, body, responseType = 'json' }) { const requestHeaders = { Accept: 'application/json', 'X-Request-Id': `client-e2e-${randomUUID()}`, ...headers } if (token) requestHeaders.Authorization = `Bearer ${token}` - let requestBody = body - if (body != null && !(body instanceof FormData)) { + const normalizedBody = path === '/api/auth/login' && body?.username && !body?.loginId + ? { ...body, loginId: body.username, username: undefined } + : body + let requestBody = normalizedBody + if (normalizedBody != null && !(normalizedBody instanceof FormData)) { requestHeaders['Content-Type'] = 'application/json' - requestBody = JSON.stringify(body) + requestBody = JSON.stringify(normalizedBody) } const response = await fetch(`${gateway}${path}`, { method,