@open-mercato/core 0.4.9-develop-ce96cffe00 → 0.4.9-develop-5d884303ad
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/modules/auth/lib/setup-app.js +17 -1
- package/dist/modules/auth/lib/setup-app.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -12
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/shipping_carriers/api/cancel/route.js +5 -1
- package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
- package/dist/modules/shipping_carriers/api/points/route.js +59 -0
- package/dist/modules/shipping_carriers/api/points/route.js.map +7 -0
- package/dist/modules/shipping_carriers/api/providers/route.js +38 -0
- package/dist/modules/shipping_carriers/api/providers/route.js.map +7 -0
- package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.js +90 -0
- package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.js.map +7 -0
- package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.meta.js +8 -0
- package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.meta.js.map +7 -0
- package/dist/modules/shipping_carriers/data/validators.js +17 -2
- package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/AddressFields.js +76 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/AddressFields.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js +243 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfirmStep.js +134 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfirmStep.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.js +70 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ProviderStep.js +32 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ProviderStep.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/WizardNav.js +37 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/components/WizardNav.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +92 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +212 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/types.js +1 -0
- package/dist/modules/shipping_carriers/lib/shipment-wizard/types.js.map +7 -0
- package/dist/modules/shipping_carriers/lib/shipping-service.js +36 -3
- package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
- package/dist/modules/shipping_carriers/lib/status-sync.js +7 -0
- package/dist/modules/shipping_carriers/lib/status-sync.js.map +2 -2
- package/package.json +7 -4
- package/src/modules/auth/lib/setup-app.ts +22 -0
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -9
- package/src/modules/shipping_carriers/api/cancel/route.ts +5 -1
- package/src/modules/shipping_carriers/api/points/route.ts +57 -0
- package/src/modules/shipping_carriers/api/providers/route.ts +35 -0
- package/src/modules/shipping_carriers/backend/shipping-carriers/create/page.meta.ts +4 -0
- package/src/modules/shipping_carriers/backend/shipping-carriers/create/page.tsx +93 -0
- package/src/modules/shipping_carriers/data/validators.ts +15 -0
- package/src/modules/shipping_carriers/i18n/de.json +66 -0
- package/src/modules/shipping_carriers/i18n/en.json +66 -0
- package/src/modules/shipping_carriers/i18n/es.json +66 -0
- package/src/modules/shipping_carriers/i18n/pl.json +66 -0
- package/src/modules/shipping_carriers/lib/adapter.ts +20 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/AddressFields.tsx +72 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.tsx +343 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/ConfirmStep.tsx +213 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.tsx +82 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/ProviderStep.tsx +54 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/components/WizardNav.tsx +46 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +153 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +312 -0
- package/src/modules/shipping_carriers/lib/shipment-wizard/types.ts +76 -0
- package/src/modules/shipping_carriers/lib/shipping-service.ts +53 -3
- package/src/modules/shipping_carriers/lib/status-sync.ts +7 -0
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
3
|
+
import { flash } from '@open-mercato/ui/backend/FlashMessages'
|
|
4
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
5
|
+
import {
|
|
6
|
+
fetchProviders,
|
|
7
|
+
fetchOrderAddresses,
|
|
8
|
+
fetchRates,
|
|
9
|
+
createShipment,
|
|
10
|
+
fetchDropOffPoints,
|
|
11
|
+
} from './shipmentApi'
|
|
12
|
+
import type {
|
|
13
|
+
WizardStep,
|
|
14
|
+
Provider,
|
|
15
|
+
Address,
|
|
16
|
+
PackageDimension,
|
|
17
|
+
ShippingRate,
|
|
18
|
+
LabelFormat,
|
|
19
|
+
DocumentAddress,
|
|
20
|
+
ContactInfo,
|
|
21
|
+
DropOffPoint,
|
|
22
|
+
} from '../types'
|
|
23
|
+
|
|
24
|
+
const EMPTY_ADDRESS: Address = { countryCode: '', postalCode: '', city: '', line1: '' }
|
|
25
|
+
const EMPTY_CONTACT: ContactInfo = { phone: '', email: '' }
|
|
26
|
+
const DEFAULT_PACKAGE: PackageDimension = { weightKg: 1, lengthCm: 20, widthCm: 15, heightCm: 10 }
|
|
27
|
+
|
|
28
|
+
const buildAddressFromDocument = (doc: DocumentAddress): Address => ({
|
|
29
|
+
countryCode: doc.country ?? '',
|
|
30
|
+
postalCode: doc.postal_code ?? '',
|
|
31
|
+
city: doc.city ?? '',
|
|
32
|
+
line1: doc.address_line1,
|
|
33
|
+
line2: doc.address_line2 ?? undefined,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const isAddressValid = (addr: Address) =>
|
|
37
|
+
addr.countryCode.length >= 2 &&
|
|
38
|
+
addr.postalCode.length > 0 &&
|
|
39
|
+
addr.city.length > 0 &&
|
|
40
|
+
addr.line1.length > 0
|
|
41
|
+
|
|
42
|
+
const isPackageValid = (pkg: PackageDimension) =>
|
|
43
|
+
pkg.weightKg > 0 && pkg.lengthCm > 0 && pkg.widthCm > 0 && pkg.heightCm > 0
|
|
44
|
+
|
|
45
|
+
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
46
|
+
const PHONE_REGEX = /^[+\d][\d\s\-().]{6,}$/
|
|
47
|
+
|
|
48
|
+
const isEmailValid = (email: string) => email === '' || EMAIL_REGEX.test(email)
|
|
49
|
+
const isPhoneValid = (phone: string) => phone === '' || PHONE_REGEX.test(phone)
|
|
50
|
+
|
|
51
|
+
export type ShipmentWizard = {
|
|
52
|
+
step: WizardStep
|
|
53
|
+
backHref: string
|
|
54
|
+
|
|
55
|
+
// Provider step
|
|
56
|
+
providers: Provider[]
|
|
57
|
+
isLoadingProviders: boolean
|
|
58
|
+
providerError: string | null
|
|
59
|
+
selectedProvider: string | null
|
|
60
|
+
|
|
61
|
+
// Configure step
|
|
62
|
+
origin: Address
|
|
63
|
+
destination: Address
|
|
64
|
+
packages: PackageDimension[]
|
|
65
|
+
labelFormat: LabelFormat
|
|
66
|
+
senderContact: ContactInfo
|
|
67
|
+
receiverContact: ContactInfo
|
|
68
|
+
targetPoint: string
|
|
69
|
+
c2cSendingMethod: string
|
|
70
|
+
isFetchingRates: boolean
|
|
71
|
+
canProceedFromConfigure: boolean
|
|
72
|
+
senderContactErrors: { email: string | null; phone: string | null }
|
|
73
|
+
receiverContactErrors: { email: string | null; phone: string | null }
|
|
74
|
+
|
|
75
|
+
// Drop-off point search
|
|
76
|
+
dropOffPointQuery: string
|
|
77
|
+
dropOffPoints: DropOffPoint[]
|
|
78
|
+
isFetchingDropOffPoints: boolean
|
|
79
|
+
dropOffPointsError: string | null
|
|
80
|
+
|
|
81
|
+
// Confirm step
|
|
82
|
+
rates: ShippingRate[]
|
|
83
|
+
ratesError: string | null
|
|
84
|
+
selectedRate: ShippingRate | null
|
|
85
|
+
isSubmitting: boolean
|
|
86
|
+
|
|
87
|
+
// Actions
|
|
88
|
+
goToStep: (step: WizardStep) => void
|
|
89
|
+
handleProviderSelect: (providerKey: string) => void
|
|
90
|
+
handleConfigureNext: () => void
|
|
91
|
+
handleSubmit: () => void
|
|
92
|
+
setOrigin: (address: Address) => void
|
|
93
|
+
setDestination: (address: Address) => void
|
|
94
|
+
setPackages: (packages: PackageDimension[]) => void
|
|
95
|
+
setLabelFormat: (format: LabelFormat) => void
|
|
96
|
+
setSenderContact: (contact: ContactInfo) => void
|
|
97
|
+
setReceiverContact: (contact: ContactInfo) => void
|
|
98
|
+
setTargetPoint: (point: string) => void
|
|
99
|
+
setC2cSendingMethod: (method: string) => void
|
|
100
|
+
setSelectedRate: (rate: ShippingRate) => void
|
|
101
|
+
searchDropOffPoints: (query: string) => void
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const useShipmentWizard = (): ShipmentWizard => {
|
|
105
|
+
const t = useT()
|
|
106
|
+
const router = useRouter()
|
|
107
|
+
const searchParams = useSearchParams()
|
|
108
|
+
const orderId = searchParams?.get('orderId') ?? null
|
|
109
|
+
|
|
110
|
+
const [step, setStep] = React.useState<WizardStep>('provider')
|
|
111
|
+
const [providers, setProviders] = React.useState<Provider[]>([])
|
|
112
|
+
const [isLoadingProviders, setIsLoadingProviders] = React.useState(true)
|
|
113
|
+
const [providerError, setProviderError] = React.useState<string | null>(null)
|
|
114
|
+
const [selectedProvider, setSelectedProvider] = React.useState<string | null>(null)
|
|
115
|
+
|
|
116
|
+
const [origin, setOrigin] = React.useState<Address>(EMPTY_ADDRESS)
|
|
117
|
+
const [destination, setDestination] = React.useState<Address>(EMPTY_ADDRESS)
|
|
118
|
+
const [packages, setPackages] = React.useState<PackageDimension[]>([DEFAULT_PACKAGE])
|
|
119
|
+
const [labelFormat, setLabelFormat] = React.useState<LabelFormat>('pdf')
|
|
120
|
+
const [senderContact, setSenderContact] = React.useState<ContactInfo>(EMPTY_CONTACT)
|
|
121
|
+
const [receiverContact, setReceiverContact] = React.useState<ContactInfo>(EMPTY_CONTACT)
|
|
122
|
+
const [targetPoint, setTargetPoint] = React.useState<string>('')
|
|
123
|
+
const [c2cSendingMethod, setC2cSendingMethod] = React.useState<string>('')
|
|
124
|
+
|
|
125
|
+
const [dropOffPointQuery, setDropOffPointQuery] = React.useState<string>('')
|
|
126
|
+
const [dropOffPoints, setDropOffPoints] = React.useState<DropOffPoint[]>([])
|
|
127
|
+
const [isFetchingDropOffPoints, setIsFetchingDropOffPoints] = React.useState(false)
|
|
128
|
+
const [dropOffPointsError, setDropOffPointsError] = React.useState<string | null>(null)
|
|
129
|
+
|
|
130
|
+
const [rates, setRates] = React.useState<ShippingRate[]>([])
|
|
131
|
+
const [isFetchingRates, setIsFetchingRates] = React.useState(false)
|
|
132
|
+
const [ratesError, setRatesError] = React.useState<string | null>(null)
|
|
133
|
+
const [selectedRate, setSelectedRate] = React.useState<ShippingRate | null>(null)
|
|
134
|
+
|
|
135
|
+
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
|
136
|
+
|
|
137
|
+
const backHref = orderId ? `/backend/sales/orders/${orderId}` : '/backend/sales/orders'
|
|
138
|
+
|
|
139
|
+
React.useEffect(() => {
|
|
140
|
+
const loadProviders = async () => {
|
|
141
|
+
setIsLoadingProviders(true)
|
|
142
|
+
setProviderError(null)
|
|
143
|
+
const result = await fetchProviders()
|
|
144
|
+
if (result.ok) {
|
|
145
|
+
setProviders(result.providers)
|
|
146
|
+
} else {
|
|
147
|
+
setProviderError(t('shipping_carriers.create.error.loadProviders', result.error))
|
|
148
|
+
}
|
|
149
|
+
setIsLoadingProviders(false)
|
|
150
|
+
}
|
|
151
|
+
void loadProviders()
|
|
152
|
+
}, [t])
|
|
153
|
+
|
|
154
|
+
React.useEffect(() => {
|
|
155
|
+
if (!orderId) return
|
|
156
|
+
const prefillAddresses = async () => {
|
|
157
|
+
const result = await fetchOrderAddresses(orderId)
|
|
158
|
+
if (!result.ok) return
|
|
159
|
+
const items = result.items
|
|
160
|
+
const shippingAddr =
|
|
161
|
+
items.find((item) => item.purpose === 'shipping') ??
|
|
162
|
+
items.find((item) => item.purpose === 'delivery') ??
|
|
163
|
+
items[0]
|
|
164
|
+
if (shippingAddr) setDestination(buildAddressFromDocument(shippingAddr))
|
|
165
|
+
}
|
|
166
|
+
void prefillAddresses()
|
|
167
|
+
}, [orderId])
|
|
168
|
+
|
|
169
|
+
const loadRates = async () => {
|
|
170
|
+
if (!selectedProvider) return
|
|
171
|
+
setIsFetchingRates(true)
|
|
172
|
+
setRatesError(null)
|
|
173
|
+
setRates([])
|
|
174
|
+
setSelectedRate(null)
|
|
175
|
+
const result = await fetchRates({
|
|
176
|
+
providerKey: selectedProvider,
|
|
177
|
+
origin,
|
|
178
|
+
destination,
|
|
179
|
+
packages,
|
|
180
|
+
...(receiverContact.phone ? { receiverPhone: receiverContact.phone } : {}),
|
|
181
|
+
...(receiverContact.email ? { receiverEmail: receiverContact.email } : {}),
|
|
182
|
+
})
|
|
183
|
+
if (result.ok) {
|
|
184
|
+
setRates(result.rates)
|
|
185
|
+
if (result.rates.length > 0) setSelectedRate(result.rates[0])
|
|
186
|
+
} else {
|
|
187
|
+
setRatesError(t('shipping_carriers.create.error.fetchRates', result.error))
|
|
188
|
+
}
|
|
189
|
+
setIsFetchingRates(false)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const handleSubmit = async () => {
|
|
193
|
+
if (!selectedProvider || !selectedRate || !orderId) return
|
|
194
|
+
setIsSubmitting(true)
|
|
195
|
+
const result = await createShipment({
|
|
196
|
+
providerKey: selectedProvider,
|
|
197
|
+
orderId,
|
|
198
|
+
origin,
|
|
199
|
+
destination,
|
|
200
|
+
packages,
|
|
201
|
+
serviceCode: selectedRate.serviceCode,
|
|
202
|
+
labelFormat,
|
|
203
|
+
...(senderContact.phone ? { senderPhone: senderContact.phone } : {}),
|
|
204
|
+
...(senderContact.email ? { senderEmail: senderContact.email } : {}),
|
|
205
|
+
...(receiverContact.phone ? { receiverPhone: receiverContact.phone } : {}),
|
|
206
|
+
...(receiverContact.email ? { receiverEmail: receiverContact.email } : {}),
|
|
207
|
+
...(targetPoint ? { targetPoint } : {}),
|
|
208
|
+
...(c2cSendingMethod ? { c2cSendingMethod } : {}),
|
|
209
|
+
})
|
|
210
|
+
setIsSubmitting(false)
|
|
211
|
+
if (result.ok) {
|
|
212
|
+
flash(t('shipping_carriers.create.success', 'Shipment created successfully.'), 'success')
|
|
213
|
+
router.push(backHref)
|
|
214
|
+
} else {
|
|
215
|
+
flash(t('shipping_carriers.create.error.create', result.error), 'error')
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const goToStep = (next: WizardStep) => setStep(next)
|
|
220
|
+
|
|
221
|
+
const handleProviderSelect = (providerKey: string) => {
|
|
222
|
+
setSelectedProvider(providerKey)
|
|
223
|
+
goToStep('configure')
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const handleConfigureNext = () => {
|
|
227
|
+
void loadRates().then(() => goToStep('confirm'))
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const searchDropOffPoints = async (query: string) => {
|
|
231
|
+
if (!selectedProvider) return
|
|
232
|
+
setDropOffPointQuery(query)
|
|
233
|
+
setIsFetchingDropOffPoints(true)
|
|
234
|
+
setDropOffPointsError(null)
|
|
235
|
+
const isPostCode = /^\d{2}-?\d{3}$/.test(query.trim())
|
|
236
|
+
const resolvedType = c2cSendingMethod === 'pop' ? 'pop' : 'parcel_locker'
|
|
237
|
+
const result = await fetchDropOffPoints({
|
|
238
|
+
providerKey: selectedProvider,
|
|
239
|
+
...(isPostCode ? { postCode: query.trim() } : { query: query.trim() }),
|
|
240
|
+
type: resolvedType,
|
|
241
|
+
})
|
|
242
|
+
if (result.ok) {
|
|
243
|
+
setDropOffPoints(result.points)
|
|
244
|
+
} else {
|
|
245
|
+
setDropOffPointsError(t('shipping_carriers.create.error.fetchDropOffPoints', result.error))
|
|
246
|
+
setDropOffPoints([])
|
|
247
|
+
}
|
|
248
|
+
setIsFetchingDropOffPoints(false)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const canProceedFromConfigure =
|
|
252
|
+
isAddressValid(origin) &&
|
|
253
|
+
isAddressValid(destination) &&
|
|
254
|
+
packages.length > 0 &&
|
|
255
|
+
packages.every(isPackageValid) &&
|
|
256
|
+
isEmailValid(senderContact.email) &&
|
|
257
|
+
isPhoneValid(senderContact.phone) &&
|
|
258
|
+
isEmailValid(receiverContact.email) &&
|
|
259
|
+
isPhoneValid(receiverContact.phone)
|
|
260
|
+
|
|
261
|
+
const senderContactErrors = {
|
|
262
|
+
email: !isEmailValid(senderContact.email) ? t('shipping_carriers.create.error.invalidEmail', 'Invalid email address.') : null,
|
|
263
|
+
phone: !isPhoneValid(senderContact.phone) ? t('shipping_carriers.create.error.invalidPhone', 'Invalid phone number.') : null,
|
|
264
|
+
}
|
|
265
|
+
const receiverContactErrors = {
|
|
266
|
+
email: !isEmailValid(receiverContact.email) ? t('shipping_carriers.create.error.invalidEmail', 'Invalid email address.') : null,
|
|
267
|
+
phone: !isPhoneValid(receiverContact.phone) ? t('shipping_carriers.create.error.invalidPhone', 'Invalid phone number.') : null,
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
step,
|
|
272
|
+
backHref,
|
|
273
|
+
providers,
|
|
274
|
+
isLoadingProviders,
|
|
275
|
+
providerError,
|
|
276
|
+
selectedProvider,
|
|
277
|
+
origin,
|
|
278
|
+
destination,
|
|
279
|
+
packages,
|
|
280
|
+
labelFormat,
|
|
281
|
+
senderContact,
|
|
282
|
+
receiverContact,
|
|
283
|
+
targetPoint,
|
|
284
|
+
c2cSendingMethod,
|
|
285
|
+
isFetchingRates,
|
|
286
|
+
canProceedFromConfigure,
|
|
287
|
+
senderContactErrors,
|
|
288
|
+
receiverContactErrors,
|
|
289
|
+
dropOffPointQuery,
|
|
290
|
+
dropOffPoints,
|
|
291
|
+
isFetchingDropOffPoints,
|
|
292
|
+
dropOffPointsError,
|
|
293
|
+
rates,
|
|
294
|
+
ratesError,
|
|
295
|
+
selectedRate,
|
|
296
|
+
isSubmitting,
|
|
297
|
+
goToStep,
|
|
298
|
+
handleProviderSelect,
|
|
299
|
+
handleConfigureNext,
|
|
300
|
+
handleSubmit: () => void handleSubmit(),
|
|
301
|
+
setOrigin,
|
|
302
|
+
setDestination,
|
|
303
|
+
setPackages,
|
|
304
|
+
setLabelFormat,
|
|
305
|
+
setSenderContact,
|
|
306
|
+
setReceiverContact,
|
|
307
|
+
setTargetPoint,
|
|
308
|
+
setC2cSendingMethod,
|
|
309
|
+
setSelectedRate,
|
|
310
|
+
searchDropOffPoints: (query: string) => void searchDropOffPoints(query),
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export type Provider = {
|
|
2
|
+
providerKey: string
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export type Address = {
|
|
6
|
+
countryCode: string
|
|
7
|
+
postalCode: string
|
|
8
|
+
city: string
|
|
9
|
+
line1: string
|
|
10
|
+
line2?: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PackageDimension = {
|
|
14
|
+
weightKg: number
|
|
15
|
+
lengthCm: number
|
|
16
|
+
widthCm: number
|
|
17
|
+
heightCm: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ShippingRate = {
|
|
21
|
+
serviceCode: string
|
|
22
|
+
serviceName: string
|
|
23
|
+
amount: number
|
|
24
|
+
currencyCode: string
|
|
25
|
+
estimatedDays?: number
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type DocumentAddress = {
|
|
29
|
+
id: string
|
|
30
|
+
purpose: string | null
|
|
31
|
+
address_line1: string
|
|
32
|
+
address_line2?: string | null
|
|
33
|
+
city?: string | null
|
|
34
|
+
postal_code?: string | null
|
|
35
|
+
country?: string | null
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type LabelFormat = 'pdf' | 'zpl' | 'png'
|
|
39
|
+
|
|
40
|
+
export type WizardStep = 'provider' | 'configure' | 'confirm'
|
|
41
|
+
|
|
42
|
+
export type AddressFieldsProps = {
|
|
43
|
+
prefix: string
|
|
44
|
+
address: Address
|
|
45
|
+
onChange: (address: Address) => void
|
|
46
|
+
disabled?: boolean
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type PackageEditorProps = {
|
|
50
|
+
packages: PackageDimension[]
|
|
51
|
+
onChange: (packages: PackageDimension[]) => void
|
|
52
|
+
disabled?: boolean
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export type ContactInfo = {
|
|
56
|
+
phone: string
|
|
57
|
+
email: string
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export type ContactFieldsProps = {
|
|
61
|
+
prefix: string
|
|
62
|
+
contact: ContactInfo
|
|
63
|
+
onChange: (contact: ContactInfo) => void
|
|
64
|
+
disabled?: boolean
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export type DropOffPoint = {
|
|
68
|
+
id: string
|
|
69
|
+
name: string
|
|
70
|
+
type: string
|
|
71
|
+
city: string
|
|
72
|
+
postalCode: string
|
|
73
|
+
street: string
|
|
74
|
+
latitude?: number
|
|
75
|
+
longitude?: number
|
|
76
|
+
}
|
|
@@ -4,6 +4,8 @@ import type { CredentialsService } from '../../integrations/lib/credentials-serv
|
|
|
4
4
|
import { CarrierShipment } from '../data/entities'
|
|
5
5
|
import { emitShippingEvent } from '../events'
|
|
6
6
|
import { getShippingAdapter } from './adapter-registry'
|
|
7
|
+
import type { UnifiedShipmentStatus } from './adapter'
|
|
8
|
+
import { isValidShippingTransition, ShipmentCancelNotAllowedError } from './status-sync'
|
|
7
9
|
|
|
8
10
|
export function createShippingCarrierService(deps: {
|
|
9
11
|
em: EntityManager
|
|
@@ -46,6 +48,8 @@ export function createShippingCarrierService(deps: {
|
|
|
46
48
|
origin: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }
|
|
47
49
|
destination: { countryCode: string; postalCode: string; city: string; line1: string; line2?: string }
|
|
48
50
|
packages: Array<{ weightKg: number; lengthCm: number; widthCm: number; heightCm: number }>
|
|
51
|
+
receiverPhone?: string
|
|
52
|
+
receiverEmail?: string
|
|
49
53
|
organizationId: string
|
|
50
54
|
tenantId: string
|
|
51
55
|
}) {
|
|
@@ -53,11 +57,16 @@ export function createShippingCarrierService(deps: {
|
|
|
53
57
|
organizationId: input.organizationId,
|
|
54
58
|
tenantId: input.tenantId,
|
|
55
59
|
})
|
|
60
|
+
const mergedCredentials = {
|
|
61
|
+
...credentials,
|
|
62
|
+
...(input.receiverPhone !== undefined ? { receiverPhone: input.receiverPhone } : {}),
|
|
63
|
+
...(input.receiverEmail !== undefined ? { receiverEmail: input.receiverEmail } : {}),
|
|
64
|
+
}
|
|
56
65
|
return adapter.calculateRates({
|
|
57
66
|
origin: input.origin,
|
|
58
67
|
destination: input.destination,
|
|
59
68
|
packages: input.packages,
|
|
60
|
-
credentials,
|
|
69
|
+
credentials: mergedCredentials,
|
|
61
70
|
})
|
|
62
71
|
},
|
|
63
72
|
|
|
@@ -69,6 +78,12 @@ export function createShippingCarrierService(deps: {
|
|
|
69
78
|
packages: Array<{ weightKg: number; lengthCm: number; widthCm: number; heightCm: number }>
|
|
70
79
|
serviceCode: string
|
|
71
80
|
labelFormat?: 'pdf' | 'zpl' | 'png'
|
|
81
|
+
senderPhone?: string
|
|
82
|
+
senderEmail?: string
|
|
83
|
+
receiverPhone?: string
|
|
84
|
+
receiverEmail?: string
|
|
85
|
+
targetPoint?: string
|
|
86
|
+
c2cSendingMethod?: string
|
|
72
87
|
organizationId: string
|
|
73
88
|
tenantId: string
|
|
74
89
|
}) {
|
|
@@ -76,13 +91,22 @@ export function createShippingCarrierService(deps: {
|
|
|
76
91
|
organizationId: input.organizationId,
|
|
77
92
|
tenantId: input.tenantId,
|
|
78
93
|
})
|
|
94
|
+
const mergedCredentials = {
|
|
95
|
+
...credentials,
|
|
96
|
+
...(input.senderPhone !== undefined ? { senderPhone: input.senderPhone } : {}),
|
|
97
|
+
...(input.senderEmail !== undefined ? { senderEmail: input.senderEmail } : {}),
|
|
98
|
+
...(input.receiverPhone !== undefined ? { receiverPhone: input.receiverPhone } : {}),
|
|
99
|
+
...(input.receiverEmail !== undefined ? { receiverEmail: input.receiverEmail } : {}),
|
|
100
|
+
...(input.targetPoint !== undefined ? { targetPoint: input.targetPoint } : {}),
|
|
101
|
+
...(input.c2cSendingMethod !== undefined ? { c2cSendingMethod: input.c2cSendingMethod } : {}),
|
|
102
|
+
}
|
|
79
103
|
const created = await adapter.createShipment({
|
|
80
104
|
orderId: input.orderId,
|
|
81
105
|
origin: input.origin,
|
|
82
106
|
destination: input.destination,
|
|
83
107
|
packages: input.packages,
|
|
84
108
|
serviceCode: input.serviceCode,
|
|
85
|
-
credentials,
|
|
109
|
+
credentials: mergedCredentials,
|
|
86
110
|
labelFormat: input.labelFormat,
|
|
87
111
|
})
|
|
88
112
|
const shipment = em.create(CarrierShipment, {
|
|
@@ -138,7 +162,7 @@ export function createShippingCarrierService(deps: {
|
|
|
138
162
|
: null
|
|
139
163
|
const tracking = await adapter.getTracking({
|
|
140
164
|
shipmentId: shipment?.carrierShipmentId ?? input.shipmentId,
|
|
141
|
-
trackingNumber: input.trackingNumber,
|
|
165
|
+
trackingNumber: input.trackingNumber ?? shipment?.trackingNumber,
|
|
142
166
|
credentials,
|
|
143
167
|
})
|
|
144
168
|
if (shipment) {
|
|
@@ -159,6 +183,9 @@ export function createShippingCarrierService(deps: {
|
|
|
159
183
|
}) {
|
|
160
184
|
const scope = { organizationId: input.organizationId, tenantId: input.tenantId }
|
|
161
185
|
const shipment = await findShipmentOrThrow(input.shipmentId, scope)
|
|
186
|
+
if (!isValidShippingTransition(shipment.unifiedStatus as UnifiedShipmentStatus, 'cancelled')) {
|
|
187
|
+
throw new ShipmentCancelNotAllowedError(shipment.unifiedStatus)
|
|
188
|
+
}
|
|
162
189
|
const { adapter, credentials } = await resolveAdapter(input.providerKey, {
|
|
163
190
|
organizationId: input.organizationId,
|
|
164
191
|
tenantId: input.tenantId,
|
|
@@ -198,6 +225,29 @@ export function createShippingCarrierService(deps: {
|
|
|
198
225
|
scope,
|
|
199
226
|
)
|
|
200
227
|
},
|
|
228
|
+
|
|
229
|
+
async searchDropOffPoints(input: {
|
|
230
|
+
providerKey: string
|
|
231
|
+
query?: string
|
|
232
|
+
type?: string
|
|
233
|
+
postCode?: string
|
|
234
|
+
organizationId: string
|
|
235
|
+
tenantId: string
|
|
236
|
+
}) {
|
|
237
|
+
const { adapter, credentials } = await resolveAdapter(input.providerKey, {
|
|
238
|
+
organizationId: input.organizationId,
|
|
239
|
+
tenantId: input.tenantId,
|
|
240
|
+
})
|
|
241
|
+
if (!adapter.searchDropOffPoints) {
|
|
242
|
+
throw new Error(`Provider ${input.providerKey} does not support drop-off point search`)
|
|
243
|
+
}
|
|
244
|
+
return adapter.searchDropOffPoints({
|
|
245
|
+
query: input.query,
|
|
246
|
+
type: input.type,
|
|
247
|
+
postCode: input.postCode,
|
|
248
|
+
credentials,
|
|
249
|
+
})
|
|
250
|
+
},
|
|
201
251
|
}
|
|
202
252
|
}
|
|
203
253
|
|
|
@@ -2,6 +2,13 @@ import type { UnifiedShipmentStatus } from './adapter'
|
|
|
2
2
|
import type { CarrierShipment } from '../data/entities'
|
|
3
3
|
import type { ShippingEventId } from '../events'
|
|
4
4
|
|
|
5
|
+
export class ShipmentCancelNotAllowedError extends Error {
|
|
6
|
+
constructor(status: string) {
|
|
7
|
+
super(`Shipment cannot be cancelled in its current status: ${status}`)
|
|
8
|
+
this.name = 'ShipmentCancelNotAllowedError'
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
const VALID_SHIPPING_TRANSITIONS: Record<string, UnifiedShipmentStatus[]> = {
|
|
6
13
|
label_created: ['picked_up', 'in_transit', 'cancelled'],
|
|
7
14
|
picked_up: ['in_transit', 'cancelled'],
|