2026-10-03 19:09:22 +08:00
import { readFile , mkdir , open } from 'node:fs/promises'
import { resolve , dirname } from 'node:path'
import { fileURLToPath , pathToFileURL } from 'node:url'
import { parseEnv } from 'node:util'
import { AcceptanceRun } from './admin-test-env-acceptance.mjs'
import { requireTestProfile , installQuoteJournal } from './fleet-quotes-test-env.mjs'
import { run as prepareCustomer } from './admin-test-env-modules/merchant-qualification-fixture.mjs'
const root = resolve ( dirname ( fileURLToPath ( import . meta . url )), '..' )
const MARKER = 'FLEET-BOOKING-QUOTE-20261003-R2'
const TRIPS = '/api/platform/v1/last-leg/fleet/trips' , QUOTES = '/api/platform/v1/last-leg/fleet/quotes'
export function warehouseQuoteContext ( detail , planned ) {
const w = detail ? . warehouse
if ( ! w ? . id || String ( w . tenantId ) !== '0' || w . status !== 'ACTIVE' || ! [ 'OVERSEAS' , 'OVERSEAS_PARTNER' ]. includes ( w . warehouseType ) || ! w . timezone ) throw Error ( 'Warehouse quote context unavailable' )
const cities = [... new Set (( detail . addresses || []). filter ( a => String ( a . warehouseId ) === String ( w . id ) && String ( a . tenantId ) === '0' && a . city ? . trim ()). map ( a => a . city . trim (). toLowerCase ()))]
if ( cities . length !== 1 ) throw Error ( 'Warehouse quote city ambiguous' )
const offset = /^(?:UTC)?([+-])(\d{1,2})(?::(\d{2}))?$/ . exec ( w . timezone )
if ( offset && ( Number ( offset [ 2 ]) > 18 || Number ( offset [ 3 ] || 0 ) > 59 || Number ( offset [ 2 ]) === 18 && Number ( offset [ 3 ] || 0 ) !== 0 )) throw Error ( 'Warehouse offset invalid' )
const day = offset ? new Date ( Date . parse ( planned ) + ( offset [ 1 ] === '-' ? - 1 : 1 ) * ( Number ( offset [ 2 ]) * 60 + Number ( offset [ 3 ] || 0 )) * 60000 ). toISOString (). slice ( 0 , 10 )
: new Intl . DateTimeFormat ( 'en-CA' , { timeZone : w . timezone , year : 'numeric' , month : '2-digit' , day : '2-digit' }). format ( new Date ( planned ))
return { warehouseId : String ( w . id ), originCity : cities [ 0 ], timezone : w . timezone , day }
}
export async function verifyBookingQuotes ( context , warehouse , planned ) {
if ( context . createdResources . bookingQuoteAcceptance || context . createdResources . merchantQualificationFixture ? . marker !== context . marker ) throw Error ( 'Fresh own fixture required; no restart' )
const proof = context . createdResources . bookingQuoteAcceptance = { marker : context . marker , status : 'CHECKING' , warehouse , quotes : [], bookings : [], checks : [], confirmationSuccess : 'NOT_TESTED_REQUIRES_LOAD_PLAN' , receiptAndDeparture : 'NOT_TESTED' }
const check = ( name , pass ) => { proof . checks . push ({ name , passed : Boolean ( pass ) }); if ( ! pass ) throw Error ( name ) }
const request = async ( method , path , body , expected = '200' ) => {
const r = await context . request ( method , path , body )
check ( ` ${ method } ${ path . split ( '?' )[ 0 ] } => ${ expected } ` , r . http === 200 && String ( r . body ? . code ) === expected && r . requestId && r . traceId )
return r . body . data
}
const customerId = String ( context . createdResources . merchantQualificationFixture . tenantId )
const destination = 'QA-BOOKING' , stops = [{ name : '本轮报价测试起运仓' }, { name : destination , destinationKey : `FBA: ${ destination } ` }]
let trip = ( await request ( 'POST' , TRIPS , { warehouseId : warehouse . warehouseId , plannedDepartureAt : planned , expectedDriverEndAt : new Date ( Date . parse ( planned ) + 3600000 ). toISOString (), stops , remark : context . marker , idempotencyKey : ` ${ context . marker } -trip` })). trip
proof . tripNo = trip . tripNo
const path = ` ${ TRIPS } / ${ trip . tripNo } /bookings`
const detail = { kind : 'SALE' , partyId : customerId , originCity : warehouse . originCity , destinationCode : destination , distanceMiles : 10 , currency : 'USD' , selfPickupPrice : null , fullDeliveryPrice : 25 , looseDeliveryPrice : 10 , validFrom : warehouse . day , validTo : warehouse . day , remark : context . marker }
const createQuote = async ( suffix , change ) => { const quote = await request ( 'POST' , QUOTES , { idempotencyKey : ` ${ context . marker } - ${ suffix } ` , details : { ... detail , ... change } }); proof . quotes . push ( quote . quoteNo ); return quote }
const valid = await createQuote ( 'valid' , {})
const expiredDay = new Date ( Date . parse ( ` ${ warehouse . day } T00:00:00Z` ) - 86400000 ). toISOString (). slice ( 0 , 10 )
await createQuote ( 'expired' , { distanceMiles : 11 , validFrom : expiredDay , validTo : expiredDay })
const cny = await createQuote ( 'cny' , { distanceMiles : 12 , currency : 'CNY' , fullDeliveryPrice : 35 , looseDeliveryPrice : null })
const zero = await createQuote ( 'zero' , { distanceMiles : 13 , fullDeliveryPrice : null , looseDeliveryPrice : 0 })
const candidates = ( currency , priceBasis ) => request ( 'GET' , ` ${ path } /quote-candidates? ${ new URLSearchParams ({ customerId , destinationKey : `FBA: ${ destination } ` , currency , priceBasis , page : '1' , size : '100' } )}` )
const pallet = await candidates ( 'USD' , 'PALLET' ), order = await candidates ( 'USD' , 'ORDER' ), cnyPallet = await candidates ( 'CNY' , 'PALLET' ), cnyOrder = await candidates ( 'CNY' , 'ORDER' )
check ( 'USD pallet candidate excludes expired/currency mismatch but preserves explicit zero' , pallet . records . length === 2 && pallet . records . some ( q => q . quoteNo === zero . quoteNo && Number ( q . quotedUnitPrice ) === 0 ) && pallet . records . some ( q => q . quoteNo === valid . quoteNo ))
check ( 'ORDER selects full-delivery instead of loose-delivery price' , order . records . length === 1 && order . records [ 0 ]. quoteNo === valid . quoteNo && order . records [ 0 ]. priceItem === 'FULL_DELIVERY' && Number ( order . records [ 0 ]. quotedUnitPrice ) === 25 )
check ( 'Missing CNY pallet price is not zero' , cnyPallet . records . length === 0 && cnyOrder . records . length === 1 && cnyOrder . records [ 0 ]. quoteNo === cny . quoteNo )
check ( 'Server date equals authoritative warehouse local planned day' , pallet . records . every ( q => q . effectiveOn === warehouse . day ))
const cargo = { destinationKey : `FBA: ${ destination } ` , cargoName : '报价联动测试' , packageCount : 2 , palletSpaces : 2 , weightKg : 10 , volumeCbm : 1 , priceBasis : 'PALLET' , unitPrice : 7 , currency : 'USD' , contactName : null , contactPhone : null , remark : context . marker }
const reference = { quoteNo : valid . quoteNo , recordVersion : valid . recordVersion , priceItem : 'LOOSE_DELIVERY' }
const firstBody = { tripRecordVersion : trip . recordVersion , customerId , cargo , quoteRef : reference , idempotencyKey : ` ${ context . marker } -booking` }
const first = await request ( 'POST' , path , firstBody ); trip . recordVersion = first . tripRecordVersion ; proof . bookings . push ( first . booking . bookingNo )
check ( 'Source price frozen separately from negotiated amount' , Number ( first . booking . quoteSource . quotedUnitPrice ) === 10 && Number ( first . booking . cargo . unitPrice ) === 7 && Number ( first . booking . agreedAmount ) === 14 && String ( first . booking . quoteSource . recordVersion ) === String ( valid . recordVersion ))
const manual = await request ( 'POST' , path , { tripRecordVersion : trip . recordVersion , customerId , cargo : { ... cargo , unitPrice : 0 }, quoteRef : null , idempotencyKey : ` ${ context . marker } -manual` }); trip . recordVersion = manual . tripRecordVersion ; proof . bookings . push ( manual . booking . bookingNo )
check ( 'Manual zero-price draft never gains a mandatory source' , manual . booking . quoteSource == null && Number ( manual . booking . agreedAmount ) === 0 )
const changed = await request ( 'PUT' , ` ${ QUOTES } / ${ valid . quoteNo } ` , { recordVersion : valid . recordVersion , details : { ... detail , looseDeliveryPrice : 12 } })
const replay = await request ( 'POST' , path , firstBody )
check ( 'Exact create replay retains original source snapshot after quote edit' , replay . replayed === true && Number ( replay . booking . quoteSource . quotedUnitPrice ) === 10 )
await request ( 'POST' , path , { ... firstBody , tripRecordVersion : trip . recordVersion , idempotencyKey : ` ${ context . marker } -stale` }, 'A0409' )
await request ( 'POST' , ` ${ path } / ${ first . booking . bookingNo } /confirm` , { tripRecordVersion : trip . recordVersion , recordVersion : first . booking . recordVersion , idempotencyKey : ` ${ context . marker } -stale-confirm` }, 'A0409' )
const afterReject = await request ( 'GET' , path )
check ( 'Stale source rejection does not mutate booking or parent version' , String ( afterReject . tripRecordVersion ) === String ( trip . recordVersion ) && afterReject . records . find ( b => b . bookingNo === first . booking . bookingNo ) ? . status === 'DRAFT' )
const refreshed = await candidates ( 'USD' , 'PALLET' ), current = refreshed . records . find ( q => q . quoteNo === valid . quoteNo )
check ( 'Explicit refresh exposes new source version and rate' , String ( current ? . recordVersion ) === String ( changed . recordVersion ) && Number ( current ? . quotedUnitPrice ) === 12 )
const updated = await request ( 'PUT' , ` ${ path } / ${ first . booking . bookingNo } ` , { tripRecordVersion : trip . recordVersion , recordVersion : first . booking . recordVersion , cargo , quoteRef : { ... reference , recordVersion : current . recordVersion } }); trip . recordVersion = updated . tripRecordVersion
check ( 'Explicit reselection refreshes source while preserving negotiated price' , Number ( updated . booking . quoteSource . quotedUnitPrice ) === 12 && Number ( updated . booking . agreedAmount ) === 14 )
const later = new Date ( Date . parse ( planned ) + 2 * 86400000 ). toISOString ()
trip = ( await request ( 'PUT' , ` ${ TRIPS } / ${ trip . tripNo } /plan` , { recordVersion : trip . recordVersion , plannedDepartureAt : later , expectedDriverEndAt : new Date ( Date . parse ( later ) + 3600000 ). toISOString (), stops , remark : context . marker , reason : '仅本轮报价有效日期验证' })). trip
check ( 'Plan date outside validity removes candidates' , ( await candidates ( 'USD' , 'PALLET' )). records . length === 0 )
await request ( 'POST' , ` ${ path } / ${ first . booking . bookingNo } /confirm` , { tripRecordVersion : trip . recordVersion , recordVersion : updated . booking . recordVersion , idempotencyKey : ` ${ context . marker } -expired-confirm` }, 'A0409' )
proof . finalBookings = await request ( 'GET' , path ); proof . status = 'VERIFIED_DRAFT_SOURCE_LINKAGE' ; proof . cleanup = 'RETAINED_OWN_PLANS_QUOTES_AND_DRAFTS_NO_RESOURCE_ASSIGNMENT'
return proof
}
export async function execute ( args = process . argv . slice ( 2 ), execArgs = process . execArgv ) {
const envFile = requireTestProfile ( args , execArgs ), env = parseEnv ( await readFile ( envFile , 'utf8' ))
if ( env . CHONGCHONG_ENV !== 'test' || ! env . CHONGCHONG_ADMIN_USERNAME || ! env . CHONGCHONG_ADMIN_PASSWORD ) throw Error ( 'TEST credentials missing' )
const base = new URL ( env . CHONGCHONG_ADMIN_API_BASE_URL )
if ( ! [ 'http:' , 'https:' ]. includes ( base . protocol ) || base . pathname . replace ( /\/$/ , '' ) !== '/api' || base . username || base . password || base . search || base . hash ) throw Error ( 'Invalid TEST API' )
// R1 stopped before creating any business fixture because its offset parser rejected valid Java ZoneId UTC-8.
// Preserve that evidence. This corrected run has a distinct claim, marker and request-key namespace.
const directory = resolve ( root , 'reports/fleet-booking-quotes-live-20261003-r2' )
await mkdir ( directory , { recursive : true })
const context = new AcceptanceRun ({ environment : 'test' , gateway : base . origin , username : env . CHONGCHONG_ADMIN_USERNAME , password : env . CHONGCHONG_ADMIN_PASSWORD , customerName : `选价测试客户- ${ MARKER } ` , quiet : true })
context . marker = MARKER
const journal = await installQuoteJournal ( context , directory , { secrets : [ env . CHONGCHONG_ADMIN_PASSWORD , env . CHONGCHONG_ADMIN_USERNAME ], persist : async ( name , data ) => {
const file = await open ( resolve ( directory , name ), 'wx' , 0o600 )
try { await file . writeFile ( JSON . stringify ({ ... data , marker : MARKER , scope : 'BOOKING_QUOTE_ONLY' , deployedCommit : '4ff55ba3e08c7fe0c6e04832fa95ebb680c76b8c' }, null , 2 )); await file . sync () } finally { await file . close () }
} })
let error = null , loggedOut = false
try {
const login = await context . request ( 'POST' , '/api/auth/login' , { loginId : env . CHONGCHONG_ADMIN_USERNAME , password : env . CHONGCHONG_ADMIN_PASSWORD }, '' )
if ( login . http !== 200 || String ( login . body ? . code ) !== '200' || ! login . body . data ? . accessToken || ! login . body . data ? . refreshToken ) throw Error ( 'Login failed' )
context . token = login . body . data . accessToken ; context . refreshToken = login . body . data . refreshToken
const options = await context . request ( 'GET' , '/api/platform/v1/last-leg/fleet/drivers/warehouse-options?region=OVERSEAS' )
if ( String ( options . body ? . code ) !== '200' || ! Array . isArray ( options . body . data )) throw Error ( 'Warehouse directory failed' )
const planned = new Date ( Date . now () + 86400000 ). toISOString (); let warehouse
for ( const option of options . body . data . slice ( 0 , 20 )) {
const detail = await context . request ( 'GET' , `/api/platform/v1/warehouse-admin/ ${ encodeURIComponent ( option . id ) } ?tenantId=0` )
if ( String ( detail . body ? . code ) !== '200' ) throw Error ( 'Warehouse details failed' )
try { warehouse = warehouseQuoteContext ( detail . body . data , planned ); break } catch { /* Read next real active warehouse; never write missing master data. */ }
}
if ( ! warehouse ) throw Error ( 'No active warehouse has unambiguous city/timezone; no business fixtures created' )
context . createdResources . quoteWarehouse = warehouse
2026-10-21 21:30:24 +08:00
await context . executeModule ( 'merchant-qualification-fixture' , () => prepareCustomer ( context , { publishEnvironment : false , requireDistinctCustomer : false }))
2026-10-03 19:09:22 +08:00
if ( context . steps . some ( s => ! s . passed )) throw Error ( 'Fresh customer fixture failed' )
await verifyBookingQuotes ( context , warehouse , planned )
} catch ( cause ) { error = cause . message ; if ( context . createdResources . bookingQuoteAcceptance ) context . createdResources . bookingQuoteAcceptance . status = 'FAILED' }
finally {
try { if ( context . token ) { const r = await context . request ( 'POST' , '/api/auth/logout' , { refreshToken : context . refreshToken }); loggedOut = r . http === 200 && String ( r . body ? . code ) === '200' } } catch { error ||= 'Logout outcome unknown' }
context . token = '' ; context . refreshToken = ''
}
const report = { success : ! error && loggedOut && ! journal . unknown (), error , loggedOut , createdResources : context . createdResources , steps : context . steps , ui : 'NOT_TESTED' , operations : 'NOT_TESTED' }
await journal . checkpoint ( 'final' , report )
return { success : report . success , directory }
}
if ( process . argv [ 1 ] && import . meta . url === pathToFileURL ( resolve ( process . argv [ 1 ])). href ) execute (). then ( r => { console . log ( JSON . stringify ( r )); process . exitCode = r . success ? 0 : 1 }). catch (() => { console . error ( 'Booking quote run blocked; preserve checkpoints, no automatic rerun.' ); process . exitCode = 1 })