@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,213 @@
1
+ "use client"
2
+
3
+ import { Button } from '@open-mercato/ui/primitives/button'
4
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
5
+ import { Card, CardContent, CardHeader, CardTitle } from '@open-mercato/ui/primitives/card'
6
+ import { Badge } from '@open-mercato/ui/primitives/badge'
7
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
8
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
9
+ import { Package, CheckCircle2 } from 'lucide-react'
10
+ import type { Address, PackageDimension, ShippingRate, LabelFormat, ContactInfo } from '../types'
11
+
12
+ export type ConfirmStepProps = {
13
+ rates: ShippingRate[]
14
+ ratesError: string | null
15
+ selectedRate: ShippingRate | null
16
+ selectedProvider: string
17
+ origin: Address
18
+ destination: Address
19
+ packages: PackageDimension[]
20
+ labelFormat: LabelFormat
21
+ senderContact: ContactInfo
22
+ receiverContact: ContactInfo
23
+ targetPoint: string
24
+ c2cSendingMethod: string
25
+ isSubmitting: boolean
26
+ onRateSelect: (rate: ShippingRate) => void
27
+ onBack: () => void
28
+ onSubmit: () => void
29
+ }
30
+
31
+ const formatAddress = (addr: Address) =>
32
+ [addr.line1, addr.city, addr.postalCode, addr.countryCode].filter(Boolean).join(', ')
33
+
34
+ export const ConfirmStep = (props: ConfirmStepProps) => {
35
+ const {
36
+ rates, ratesError, selectedRate, selectedProvider,
37
+ origin, destination, packages, labelFormat,
38
+ senderContact, receiverContact, targetPoint, c2cSendingMethod,
39
+ isSubmitting, onRateSelect, onBack, onSubmit,
40
+ } = props
41
+ const t = useT()
42
+
43
+ return (
44
+ <section className="space-y-6">
45
+ {ratesError ? (
46
+ <div className="space-y-3">
47
+ <ErrorMessage label={ratesError} />
48
+ <Button type="button" variant="outline" size="sm" onClick={onBack}>
49
+ {t('shipping_carriers.create.action.editConfiguration', 'Edit configuration')}
50
+ </Button>
51
+ </div>
52
+ ) : null}
53
+
54
+ {rates.length === 0 && !ratesError ? (
55
+ <Card>
56
+ <CardContent className="py-8 text-center text-sm text-muted-foreground">
57
+ {t('shipping_carriers.create.noRates', 'No rates available for this route and package configuration.')}
58
+ </CardContent>
59
+ </Card>
60
+ ) : (
61
+ <Card>
62
+ <CardHeader>
63
+ <CardTitle className="text-base">
64
+ {t('shipping_carriers.create.section.selectRate', 'Select service')}
65
+ </CardTitle>
66
+ </CardHeader>
67
+ <CardContent>
68
+ <div className="space-y-2">
69
+ {rates.map((rate) => {
70
+ const isSelected = selectedRate?.serviceCode === rate.serviceCode
71
+ return (
72
+ <button
73
+ key={rate.serviceCode}
74
+ type="button"
75
+ className={`flex w-full cursor-pointer items-center justify-between rounded-lg border p-3 text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
76
+ isSelected ? 'border-primary bg-primary/5' : 'hover:bg-muted/50'
77
+ }`}
78
+ onClick={() => onRateSelect(rate)}
79
+ >
80
+ <div className="flex items-center gap-3">
81
+ <Package className="h-4 w-4 shrink-0 text-muted-foreground" />
82
+ <div>
83
+ <p className="font-medium">{rate.serviceName}</p>
84
+ <p className="text-xs text-muted-foreground">{rate.serviceCode}</p>
85
+ </div>
86
+ </div>
87
+ <div className="flex items-center gap-3">
88
+ {rate.estimatedDays !== undefined ? (
89
+ <span className="text-xs text-muted-foreground">
90
+ {t('shipping_carriers.create.estimatedDays', '{days} day(s)', { days: rate.estimatedDays })}
91
+ </span>
92
+ ) : null}
93
+ <Badge variant="secondary">
94
+ {rate.amount.toFixed(2)} {rate.currencyCode}
95
+ </Badge>
96
+ {isSelected ? <CheckCircle2 className="h-4 w-4 text-primary" /> : null}
97
+ </div>
98
+ </button>
99
+ )
100
+ })}
101
+ </div>
102
+ </CardContent>
103
+ </Card>
104
+ )}
105
+
106
+ <Card>
107
+ <CardHeader>
108
+ <CardTitle className="text-base">
109
+ {t('shipping_carriers.create.section.summary', 'Summary')}
110
+ </CardTitle>
111
+ </CardHeader>
112
+ <CardContent>
113
+ <dl className="grid gap-2 text-sm sm:grid-cols-2">
114
+ <div>
115
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
116
+ {t('shipping_carriers.create.summary.carrier', 'Carrier')}
117
+ </dt>
118
+ <dd className="mt-0.5 capitalize">{selectedProvider.replace(/_/g, ' ')}</dd>
119
+ </div>
120
+ {selectedRate ? (
121
+ <div>
122
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
123
+ {t('shipping_carriers.create.summary.service', 'Service')}
124
+ </dt>
125
+ <dd className="mt-0.5">{selectedRate.serviceName}</dd>
126
+ </div>
127
+ ) : null}
128
+ <div>
129
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
130
+ {t('shipping_carriers.create.summary.from', 'From')}
131
+ </dt>
132
+ <dd className="mt-0.5">{formatAddress(origin)}</dd>
133
+ </div>
134
+ <div>
135
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
136
+ {t('shipping_carriers.create.summary.to', 'To')}
137
+ </dt>
138
+ <dd className="mt-0.5">{formatAddress(destination)}</dd>
139
+ </div>
140
+ <div>
141
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
142
+ {t('shipping_carriers.create.summary.packages', 'Packages')}
143
+ </dt>
144
+ <dd className="mt-0.5">{packages.length}</dd>
145
+ </div>
146
+ <div>
147
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
148
+ {t('shipping_carriers.create.summary.labelFormat', 'Label format')}
149
+ </dt>
150
+ <dd className="mt-0.5 uppercase">{labelFormat}</dd>
151
+ </div>
152
+ {(senderContact.phone || senderContact.email) ? (
153
+ <div>
154
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
155
+ {t('shipping_carriers.create.summary.senderContact', 'Sender contact')}
156
+ </dt>
157
+ <dd className="mt-0.5">
158
+ {[senderContact.phone, senderContact.email].filter(Boolean).join(' / ')}
159
+ </dd>
160
+ </div>
161
+ ) : null}
162
+ {(receiverContact.phone || receiverContact.email) ? (
163
+ <div>
164
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
165
+ {t('shipping_carriers.create.summary.receiverContact', 'Receiver contact')}
166
+ </dt>
167
+ <dd className="mt-0.5">
168
+ {[receiverContact.phone, receiverContact.email].filter(Boolean).join(' / ')}
169
+ </dd>
170
+ </div>
171
+ ) : null}
172
+ {targetPoint ? (
173
+ <div>
174
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
175
+ {t('shipping_carriers.create.summary.targetPoint', 'Locker point')}
176
+ </dt>
177
+ <dd className="mt-0.5">{targetPoint}</dd>
178
+ </div>
179
+ ) : null}
180
+ {c2cSendingMethod ? (
181
+ <div>
182
+ <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">
183
+ {t('shipping_carriers.create.summary.c2cSendingMethod', 'C2C sending method')}
184
+ </dt>
185
+ <dd className="mt-0.5">{c2cSendingMethod}</dd>
186
+ </div>
187
+ ) : null}
188
+ </dl>
189
+ </CardContent>
190
+ </Card>
191
+
192
+ <div className="flex justify-between gap-3">
193
+ <Button type="button" variant="outline" onClick={onBack} disabled={isSubmitting}>
194
+ {t('shipping_carriers.create.back', 'Back')}
195
+ </Button>
196
+ <Button
197
+ type="button"
198
+ onClick={onSubmit}
199
+ disabled={!selectedRate || isSubmitting}
200
+ >
201
+ {isSubmitting ? (
202
+ <>
203
+ <Spinner className="mr-2 h-4 w-4" />
204
+ {t('shipping_carriers.create.submitting', 'Creating shipment...')}
205
+ </>
206
+ ) : (
207
+ t('shipping_carriers.create.submit', 'Create shipment')
208
+ )}
209
+ </Button>
210
+ </div>
211
+ </section>
212
+ )
213
+ }
@@ -0,0 +1,82 @@
1
+ "use client"
2
+
3
+ import { Button } from '@open-mercato/ui/primitives/button'
4
+ import { Input } from '@open-mercato/ui/primitives/input'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import type { PackageDimension, PackageEditorProps } from '../types'
7
+
8
+ const PACKAGE_FIELDS = ['weightKg', 'lengthCm', 'widthCm', 'heightCm'] as const
9
+
10
+ const DEFAULT_PACKAGE: PackageDimension = { weightKg: 1, lengthCm: 20, widthCm: 15, heightCm: 10 }
11
+
12
+ export const PackageEditor = (props: PackageEditorProps) => {
13
+ const { packages, onChange, disabled } = props
14
+ const t = useT()
15
+
16
+ const fieldLabel = (field: keyof PackageDimension) => {
17
+ const labels: Record<keyof PackageDimension, string> = {
18
+ weightKg: t('shipping_carriers.create.package.weightKg', 'Weight (kg)'),
19
+ lengthCm: t('shipping_carriers.create.package.lengthCm', 'Length (cm)'),
20
+ widthCm: t('shipping_carriers.create.package.widthCm', 'Width (cm)'),
21
+ heightCm: t('shipping_carriers.create.package.heightCm', 'Height (cm)'),
22
+ }
23
+ return labels[field]
24
+ }
25
+
26
+ const updatePackage = (index: number, field: keyof PackageDimension, raw: string) => {
27
+ const value = parseFloat(raw)
28
+ onChange(packages.map((pkg, idx) =>
29
+ idx === index ? { ...pkg, [field]: Number.isNaN(value) ? 0 : value } : pkg,
30
+ ))
31
+ }
32
+
33
+ const addPackage = () => onChange([...packages, DEFAULT_PACKAGE])
34
+
35
+ const removePackage = (index: number) => onChange(packages.filter((_, idx) => idx !== index))
36
+
37
+ return (
38
+ <div className="space-y-3">
39
+ {packages.map((pkg, index) => (
40
+ <div key={index} className="rounded-lg border bg-muted/20 p-3">
41
+ <div className="mb-2 flex items-center justify-between">
42
+ <span className="text-xs font-medium text-muted-foreground">
43
+ {t('shipping_carriers.create.package.label', 'Package')} {index + 1}
44
+ </span>
45
+ {packages.length > 1 ? (
46
+ <Button
47
+ type="button"
48
+ variant="ghost"
49
+ size="sm"
50
+ className="h-auto px-2 py-0.5 text-xs text-red-600 hover:text-red-700"
51
+ onClick={() => removePackage(index)}
52
+ disabled={disabled}
53
+ >
54
+ {t('shipping_carriers.create.package.remove', 'Remove')}
55
+ </Button>
56
+ ) : null}
57
+ </div>
58
+ <div className="grid gap-3 sm:grid-cols-4">
59
+ {PACKAGE_FIELDS.map((field) => (
60
+ <div key={field}>
61
+ <label className="mb-1 block text-xs font-medium text-muted-foreground">
62
+ {fieldLabel(field)}
63
+ </label>
64
+ <Input
65
+ type="number"
66
+ min="0.01"
67
+ step="0.01"
68
+ value={pkg[field]}
69
+ onChange={(event) => updatePackage(index, field, event.target.value)}
70
+ disabled={disabled}
71
+ />
72
+ </div>
73
+ ))}
74
+ </div>
75
+ </div>
76
+ ))}
77
+ <Button type="button" variant="outline" size="sm" onClick={addPackage} disabled={disabled}>
78
+ {t('shipping_carriers.create.package.add', '+ Add package')}
79
+ </Button>
80
+ </div>
81
+ )
82
+ }
@@ -0,0 +1,54 @@
1
+ "use client"
2
+
3
+ import { match, P } from 'ts-pattern'
4
+ import { Card, CardContent } from '@open-mercato/ui/primitives/card'
5
+ import { ErrorMessage } from '@open-mercato/ui/backend/detail'
6
+ import { Spinner } from '@open-mercato/ui/primitives/spinner'
7
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
8
+ import { Truck } from 'lucide-react'
9
+ import type { Provider } from '../types'
10
+
11
+ export type ProviderStepProps = {
12
+ isLoading: boolean
13
+ error: string | null
14
+ providers: Provider[]
15
+ onSelect: (providerKey: string) => void
16
+ }
17
+
18
+ export const ProviderStep = (props: ProviderStepProps) => {
19
+ const { providers, onSelect } = props
20
+ const t = useT()
21
+
22
+ return match(props)
23
+ .with({ isLoading: true }, () => (
24
+ <div className="flex justify-center py-8"><Spinner /></div>
25
+ ))
26
+ .with({ error: P.string }, ({ error }) => (
27
+ <ErrorMessage label={error} />
28
+ ))
29
+ .with({ providers: [] }, () => (
30
+ <Card>
31
+ <CardContent className="py-8 text-center text-sm text-muted-foreground">
32
+ {t('shipping_carriers.create.noProviders', 'No shipping providers are configured. Enable a carrier integration first.')}
33
+ </CardContent>
34
+ </Card>
35
+ ))
36
+ .otherwise(() => (
37
+ <div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
38
+ {providers.map((provider) => (
39
+ <button
40
+ key={provider.providerKey}
41
+ type="button"
42
+ className="flex cursor-pointer items-center gap-3 rounded-lg border bg-card p-4 text-left transition-colors hover:border-primary hover:bg-primary/5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
43
+ onClick={() => onSelect(provider.providerKey)}
44
+ >
45
+ <Truck className="h-6 w-6 shrink-0 text-muted-foreground" />
46
+ <div>
47
+ <p className="font-medium capitalize">{provider.providerKey.replace(/_/g, ' ')}</p>
48
+ <p className="text-xs text-muted-foreground">{provider.providerKey}</p>
49
+ </div>
50
+ </button>
51
+ ))}
52
+ </div>
53
+ ))
54
+ }
@@ -0,0 +1,46 @@
1
+ "use client"
2
+
3
+ import * as React from 'react'
4
+ import { match } from 'ts-pattern'
5
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
6
+ import type { WizardStep } from '../types'
7
+
8
+ const STEP_ORDER: WizardStep[] = ['provider', 'configure', 'confirm']
9
+
10
+ export type WizardNavProps = {
11
+ step: WizardStep
12
+ onNavigate: (step: WizardStep) => void
13
+ }
14
+
15
+ export const WizardNav = (props: WizardNavProps) => {
16
+ const { step, onNavigate } = props
17
+ const t = useT()
18
+ const stepIndex = STEP_ORDER.indexOf(step)
19
+
20
+ const stepLabels: Record<WizardStep, string> = {
21
+ provider: t('shipping_carriers.create.step.provider', 'Select carrier'),
22
+ configure: t('shipping_carriers.create.step.configure', 'Configure shipment'),
23
+ confirm: t('shipping_carriers.create.step.confirm', 'Select service & confirm'),
24
+ }
25
+
26
+ return (
27
+ <nav className="flex items-center gap-2 text-sm text-muted-foreground">
28
+ {STEP_ORDER.map((stepId, index) => (
29
+ <React.Fragment key={stepId}>
30
+ {index > 0 ? <span aria-hidden>›</span> : null}
31
+ <span
32
+ className={match({ isCurrent: stepId === step, isPast: index < stepIndex })
33
+ .with({ isCurrent: true }, () => 'font-medium text-foreground')
34
+ .with({ isPast: true }, () => 'cursor-pointer hover:text-foreground')
35
+ .otherwise(() => '')}
36
+ onClick={() => {
37
+ if (index < stepIndex) onNavigate(stepId)
38
+ }}
39
+ >
40
+ {index + 1}. {stepLabels[stepId]}
41
+ </span>
42
+ </React.Fragment>
43
+ ))}
44
+ </nav>
45
+ )
46
+ }
@@ -0,0 +1,153 @@
1
+ import { match, P } from 'ts-pattern'
2
+ import { apiCall } from '@open-mercato/ui/backend/utils/apiCall'
3
+ import type { Provider, Address, PackageDimension, ShippingRate, DocumentAddress, DropOffPoint } from '../types'
4
+
5
+ export type FetchProvidersResult =
6
+ | { ok: true; providers: Provider[] }
7
+ | { ok: false; error: string }
8
+
9
+ export type FetchOrderAddressesResult =
10
+ | { ok: true; items: DocumentAddress[] }
11
+ | { ok: false }
12
+
13
+ export type FetchRatesParams = {
14
+ providerKey: string
15
+ origin: Address
16
+ destination: Address
17
+ packages: PackageDimension[]
18
+ receiverPhone?: string
19
+ receiverEmail?: string
20
+ }
21
+
22
+ export type FetchRatesResult =
23
+ | { ok: true; rates: ShippingRate[] }
24
+ | { ok: false; error: string }
25
+
26
+ export type CreateShipmentParams = {
27
+ providerKey: string
28
+ orderId: string
29
+ origin: Address
30
+ destination: Address
31
+ packages: PackageDimension[]
32
+ serviceCode: string
33
+ labelFormat: string
34
+ senderPhone?: string
35
+ senderEmail?: string
36
+ receiverPhone?: string
37
+ receiverEmail?: string
38
+ targetPoint?: string
39
+ c2cSendingMethod?: string
40
+ }
41
+
42
+ export type CreateShipmentResult =
43
+ | { ok: true }
44
+ | { ok: false; error: string }
45
+
46
+ export type FetchDropOffPointsParams = {
47
+ providerKey: string
48
+ query?: string
49
+ type?: string
50
+ postCode?: string
51
+ }
52
+
53
+ export type FetchDropOffPointsResult =
54
+ | { ok: true; points: DropOffPoint[] }
55
+ | { ok: false; error: string }
56
+
57
+ export const fetchProviders = async (): Promise<FetchProvidersResult> => {
58
+ const call = await apiCall<{ providers: Provider[] }>(
59
+ '/api/shipping-carriers/providers',
60
+ undefined,
61
+ { fallback: { providers: [] } },
62
+ )
63
+ return match(call)
64
+ .with({ ok: true, result: P.not(P.nullish) }, ({ result }) => ({
65
+ ok: true as const,
66
+ providers: result.providers,
67
+ }))
68
+ .otherwise(() => ({ ok: false as const, error: 'Failed to load shipping providers.' }))
69
+ }
70
+
71
+ export const fetchOrderAddresses = async (orderId: string): Promise<FetchOrderAddressesResult> => {
72
+ const call = await apiCall<{ items: DocumentAddress[] }>(
73
+ `/api/sales/document-addresses?documentId=${orderId}&documentKind=order&pageSize=50`,
74
+ undefined,
75
+ { fallback: { items: [] } },
76
+ )
77
+ return match(call)
78
+ .with({ ok: true, result: P.not(P.nullish) }, ({ result }) => ({
79
+ ok: true as const,
80
+ items: result.items,
81
+ }))
82
+ .otherwise(() => ({ ok: false as const }))
83
+ }
84
+
85
+ export const fetchRates = async (params: FetchRatesParams): Promise<FetchRatesResult> => {
86
+ const { providerKey, origin, destination, packages, receiverPhone, receiverEmail } = params
87
+ const call = await apiCall<{ rates: ShippingRate[] }>(
88
+ '/api/shipping-carriers/rates',
89
+ {
90
+ method: 'POST',
91
+ headers: { 'Content-Type': 'application/json' },
92
+ body: JSON.stringify({
93
+ providerKey,
94
+ origin,
95
+ destination,
96
+ packages,
97
+ ...(receiverPhone !== undefined ? { receiverPhone } : {}),
98
+ ...(receiverEmail !== undefined ? { receiverEmail } : {}),
99
+ }),
100
+ },
101
+ { fallback: { rates: [] } },
102
+ )
103
+ return match(call)
104
+ .with({ ok: true, result: P.not(P.nullish) }, ({ result }) => ({
105
+ ok: true as const,
106
+ rates: result.rates,
107
+ }))
108
+ .otherwise(({ result }) => ({
109
+ ok: false as const,
110
+ error: (result as { error?: string } | null)?.error ?? 'Failed to fetch shipping rates.',
111
+ }))
112
+ }
113
+
114
+ export const createShipment = async (params: CreateShipmentParams): Promise<CreateShipmentResult> => {
115
+ const call = await apiCall(
116
+ '/api/shipping-carriers/shipments',
117
+ {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify(params),
121
+ },
122
+ { fallback: null },
123
+ )
124
+ return match(call)
125
+ .with({ ok: true }, () => ({ ok: true as const }))
126
+ .otherwise(({ result }) => ({
127
+ ok: false as const,
128
+ error: (result as { error?: string } | null)?.error ?? 'Failed to create shipment.',
129
+ }))
130
+ }
131
+
132
+ export const fetchDropOffPoints = async (params: FetchDropOffPointsParams): Promise<FetchDropOffPointsResult> => {
133
+ const url = new URL('/api/shipping-carriers/points', 'http://placeholder')
134
+ url.searchParams.set('providerKey', params.providerKey)
135
+ if (params.query) url.searchParams.set('query', params.query)
136
+ if (params.type) url.searchParams.set('type', params.type)
137
+ if (params.postCode) url.searchParams.set('postCode', params.postCode)
138
+
139
+ const call = await apiCall<{ points: DropOffPoint[] }>(
140
+ `${url.pathname}${url.search}`,
141
+ undefined,
142
+ { fallback: { points: [] } },
143
+ )
144
+ return match(call)
145
+ .with({ ok: true, result: P.not(P.nullish) }, ({ result }) => ({
146
+ ok: true as const,
147
+ points: result.points,
148
+ }))
149
+ .otherwise(({ result }) => ({
150
+ ok: false as const,
151
+ error: (result as { error?: string } | null)?.error ?? 'Failed to fetch drop-off points.',
152
+ }))
153
+ }