@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.
Files changed (63) hide show
  1. package/dist/modules/auth/lib/setup-app.js +17 -1
  2. package/dist/modules/auth/lib/setup-app.js.map +2 -2
  3. package/dist/modules/sales/backend/sales/documents/[id]/page.js +23 -12
  4. package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
  5. package/dist/modules/shipping_carriers/api/cancel/route.js +5 -1
  6. package/dist/modules/shipping_carriers/api/cancel/route.js.map +2 -2
  7. package/dist/modules/shipping_carriers/api/points/route.js +59 -0
  8. package/dist/modules/shipping_carriers/api/points/route.js.map +7 -0
  9. package/dist/modules/shipping_carriers/api/providers/route.js +38 -0
  10. package/dist/modules/shipping_carriers/api/providers/route.js.map +7 -0
  11. package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.js +90 -0
  12. package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.js.map +7 -0
  13. package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.meta.js +8 -0
  14. package/dist/modules/shipping_carriers/backend/shipping-carriers/create/page.meta.js.map +7 -0
  15. package/dist/modules/shipping_carriers/data/validators.js +17 -2
  16. package/dist/modules/shipping_carriers/data/validators.js.map +2 -2
  17. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/AddressFields.js +76 -0
  18. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/AddressFields.js.map +7 -0
  19. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js +243 -0
  20. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.js.map +7 -0
  21. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfirmStep.js +134 -0
  22. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ConfirmStep.js.map +7 -0
  23. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.js +70 -0
  24. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.js.map +7 -0
  25. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ProviderStep.js +32 -0
  26. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/ProviderStep.js.map +7 -0
  27. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/WizardNav.js +37 -0
  28. package/dist/modules/shipping_carriers/lib/shipment-wizard/components/WizardNav.js.map +7 -0
  29. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js +92 -0
  30. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.js.map +7 -0
  31. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js +212 -0
  32. package/dist/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.js.map +7 -0
  33. package/dist/modules/shipping_carriers/lib/shipment-wizard/types.js +1 -0
  34. package/dist/modules/shipping_carriers/lib/shipment-wizard/types.js.map +7 -0
  35. package/dist/modules/shipping_carriers/lib/shipping-service.js +36 -3
  36. package/dist/modules/shipping_carriers/lib/shipping-service.js.map +2 -2
  37. package/dist/modules/shipping_carriers/lib/status-sync.js +7 -0
  38. package/dist/modules/shipping_carriers/lib/status-sync.js.map +2 -2
  39. package/package.json +7 -4
  40. package/src/modules/auth/lib/setup-app.ts +22 -0
  41. package/src/modules/sales/backend/sales/documents/[id]/page.tsx +17 -9
  42. package/src/modules/shipping_carriers/api/cancel/route.ts +5 -1
  43. package/src/modules/shipping_carriers/api/points/route.ts +57 -0
  44. package/src/modules/shipping_carriers/api/providers/route.ts +35 -0
  45. package/src/modules/shipping_carriers/backend/shipping-carriers/create/page.meta.ts +4 -0
  46. package/src/modules/shipping_carriers/backend/shipping-carriers/create/page.tsx +93 -0
  47. package/src/modules/shipping_carriers/data/validators.ts +15 -0
  48. package/src/modules/shipping_carriers/i18n/de.json +66 -0
  49. package/src/modules/shipping_carriers/i18n/en.json +66 -0
  50. package/src/modules/shipping_carriers/i18n/es.json +66 -0
  51. package/src/modules/shipping_carriers/i18n/pl.json +66 -0
  52. package/src/modules/shipping_carriers/lib/adapter.ts +20 -0
  53. package/src/modules/shipping_carriers/lib/shipment-wizard/components/AddressFields.tsx +72 -0
  54. package/src/modules/shipping_carriers/lib/shipment-wizard/components/ConfigureStep.tsx +343 -0
  55. package/src/modules/shipping_carriers/lib/shipment-wizard/components/ConfirmStep.tsx +213 -0
  56. package/src/modules/shipping_carriers/lib/shipment-wizard/components/PackageEditor.tsx +82 -0
  57. package/src/modules/shipping_carriers/lib/shipment-wizard/components/ProviderStep.tsx +54 -0
  58. package/src/modules/shipping_carriers/lib/shipment-wizard/components/WizardNav.tsx +46 -0
  59. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/shipmentApi.ts +153 -0
  60. package/src/modules/shipping_carriers/lib/shipment-wizard/hooks/useShipmentWizard.ts +312 -0
  61. package/src/modules/shipping_carriers/lib/shipment-wizard/types.ts +76 -0
  62. package/src/modules/shipping_carriers/lib/shipping-service.ts +53 -3
  63. 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'],