@open-mercato/core 0.10.1-develop.7723.1.3b2ea82ea7 → 0.10.1-develop.7724.1.95e4e7d38e
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/.turbo/turbo-build.log +1 -1
- package/dist/generated/entities/sales_order/index.js +2 -0
- package/dist/generated/entities/sales_order/index.js.map +2 -2
- package/dist/generated/entities/sales_order_line/index.js +2 -0
- package/dist/generated/entities/sales_order_line/index.js.map +2 -2
- package/dist/generated/entity-fields-registry.js +2 -0
- package/dist/generated/entity-fields-registry.js.map +2 -2
- package/dist/modules/sales/api/documents/factory.js +3 -0
- package/dist/modules/sales/api/documents/factory.js.map +2 -2
- package/dist/modules/sales/api/order-lines/route.js +2 -1
- package/dist/modules/sales/api/order-lines/route.js.map +2 -2
- package/dist/modules/sales/backend/sales/documents/[id]/page.js +83 -6
- package/dist/modules/sales/backend/sales/documents/[id]/page.js.map +2 -2
- package/dist/modules/sales/commands/documents.js +141 -18
- package/dist/modules/sales/commands/documents.js.map +2 -2
- package/dist/modules/sales/commands/returns.js +24 -7
- package/dist/modules/sales/commands/returns.js.map +2 -2
- package/dist/modules/sales/components/documents/AdjustmentsSection.js +17 -11
- package/dist/modules/sales/components/documents/AdjustmentsSection.js.map +2 -2
- package/dist/modules/sales/components/documents/DocumentTotals.js +21 -3
- package/dist/modules/sales/components/documents/DocumentTotals.js.map +2 -2
- package/dist/modules/sales/components/documents/ItemsSection.js +23 -12
- package/dist/modules/sales/components/documents/ItemsSection.js.map +2 -2
- package/dist/modules/sales/data/entities.js +10 -0
- package/dist/modules/sales/data/entities.js.map +2 -2
- package/dist/modules/sales/data/validators.js +9 -1
- package/dist/modules/sales/data/validators.js.map +2 -2
- package/dist/modules/sales/lib/calculations.js +85 -6
- package/dist/modules/sales/lib/calculations.js.map +2 -2
- package/dist/modules/sales/lib/externalAmounts.js +143 -0
- package/dist/modules/sales/lib/externalAmounts.js.map +7 -0
- package/dist/modules/sales/lib/lineSnapshots.js +6 -1
- package/dist/modules/sales/lib/lineSnapshots.js.map +2 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js +8 -2
- package/dist/modules/sales/lib/makeSalesLineRoute.js.map +2 -2
- package/dist/modules/sales/lib/providers/totals.js +3 -2
- package/dist/modules/sales/lib/providers/totals.js.map +2 -2
- package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js +19 -0
- package/dist/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.js.map +7 -0
- package/generated/entities/sales_order/index.ts +1 -0
- package/generated/entities/sales_order_line/index.ts +1 -0
- package/generated/entity-fields-registry.ts +2 -0
- package/package.json +7 -7
- package/src/modules/sales/api/documents/factory.ts +3 -0
- package/src/modules/sales/api/order-lines/route.ts +2 -1
- package/src/modules/sales/backend/sales/documents/[id]/page.tsx +96 -0
- package/src/modules/sales/commands/documents.ts +219 -44
- package/src/modules/sales/commands/returns.ts +27 -3
- package/src/modules/sales/components/documents/AdjustmentsSection.tsx +34 -25
- package/src/modules/sales/components/documents/DocumentTotals.tsx +26 -2
- package/src/modules/sales/components/documents/ItemsSection.tsx +27 -12
- package/src/modules/sales/data/entities.ts +14 -0
- package/src/modules/sales/data/validators.ts +10 -0
- package/src/modules/sales/i18n/de.json +12 -0
- package/src/modules/sales/i18n/en.json +12 -0
- package/src/modules/sales/i18n/es.json +12 -0
- package/src/modules/sales/i18n/ko.json +12 -0
- package/src/modules/sales/i18n/pl.json +12 -0
- package/src/modules/sales/lib/calculations.ts +99 -4
- package/src/modules/sales/lib/externalAmounts.ts +174 -0
- package/src/modules/sales/lib/lineSnapshots.ts +6 -1
- package/src/modules/sales/lib/makeSalesLineRoute.ts +8 -0
- package/src/modules/sales/lib/providers/totals.ts +5 -2
- package/src/modules/sales/lib/types.ts +19 -2
- package/src/modules/sales/migrations/.snapshot-open-mercato.json +33 -1
- package/src/modules/sales/migrations/Migration20260910120000_sales_external_amounts_mode.ts +25 -0
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { CrudHttpError } from '@open-mercato/shared/lib/crud/errors'
|
|
2
|
+
import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
|
|
3
|
+
import type { SalesAmountsMode } from '../data/entities'
|
|
4
|
+
import { EXTERNAL_HEADER_FIELDS } from './calculations'
|
|
5
|
+
import type { SalesDocumentAmounts } from './types'
|
|
6
|
+
|
|
7
|
+
export type ExternalHeaderField = (typeof EXTERNAL_HEADER_FIELDS)[number]
|
|
8
|
+
|
|
9
|
+
// A mirrored document that charges no shipping or surcharge has nothing to say
|
|
10
|
+
// about them, so they default to zero instead of being required.
|
|
11
|
+
const OPTIONAL_EXTERNAL_HEADER_FIELDS: ReadonlySet<ExternalHeaderField> = new Set([
|
|
12
|
+
'shippingNetAmount',
|
|
13
|
+
'shippingGrossAmount',
|
|
14
|
+
'surchargeTotalAmount',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
export const REQUIRED_EXTERNAL_HEADER_FIELDS: readonly ExternalHeaderField[] =
|
|
18
|
+
EXTERNAL_HEADER_FIELDS.filter((field) => !OPTIONAL_EXTERNAL_HEADER_FIELDS.has(field))
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `unitPriceNet` is required because the derived discount consumes it: without it
|
|
22
|
+
* the line's whole net would be stored as a discount, indistinguishable from a markup.
|
|
23
|
+
*/
|
|
24
|
+
export const REQUIRED_EXTERNAL_LINE_FIELDS = [
|
|
25
|
+
'unitPriceNet',
|
|
26
|
+
'totalNetAmount',
|
|
27
|
+
'totalGrossAmount',
|
|
28
|
+
'taxAmount',
|
|
29
|
+
] as const
|
|
30
|
+
|
|
31
|
+
export type ExternalLineField = (typeof REQUIRED_EXTERNAL_LINE_FIELDS)[number]
|
|
32
|
+
|
|
33
|
+
function toFiniteNumber(value: unknown): number | null {
|
|
34
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value
|
|
35
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
36
|
+
const parsed = Number(value)
|
|
37
|
+
return Number.isFinite(parsed) ? parsed : null
|
|
38
|
+
}
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
type HeaderSource = Record<string, unknown> | null | undefined
|
|
43
|
+
|
|
44
|
+
export function collectSuppliedHeaderTotals(source: HeaderSource): Partial<SalesDocumentAmounts> {
|
|
45
|
+
const totals: Partial<SalesDocumentAmounts> = {}
|
|
46
|
+
if (!source) return totals
|
|
47
|
+
for (const field of EXTERNAL_HEADER_FIELDS) {
|
|
48
|
+
const value = toFiniteNumber(source[field])
|
|
49
|
+
if (value !== null) totals[field] = value
|
|
50
|
+
}
|
|
51
|
+
return totals
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function hasAnySuppliedHeaderTotal(source: HeaderSource): boolean {
|
|
55
|
+
if (!source) return false
|
|
56
|
+
return EXTERNAL_HEADER_FIELDS.some((field) => toFiniteNumber(source[field]) !== null)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function missingHeaderFields(source: HeaderSource): ExternalHeaderField[] {
|
|
60
|
+
return REQUIRED_EXTERNAL_HEADER_FIELDS.filter(
|
|
61
|
+
(field) => toFiniteNumber(source?.[field]) === null,
|
|
62
|
+
)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function missingLineFields(line: Record<string, unknown>): ExternalLineField[] {
|
|
66
|
+
return REQUIRED_EXTERNAL_LINE_FIELDS.filter((field) => toFiniteNumber(line[field]) === null)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The supplied header with its optional fields defaulted to zero. */
|
|
70
|
+
export function buildExternalHeaderTotals(source: HeaderSource): Partial<SalesDocumentAmounts> {
|
|
71
|
+
const supplied = collectSuppliedHeaderTotals(source)
|
|
72
|
+
for (const field of OPTIONAL_EXTERNAL_HEADER_FIELDS) {
|
|
73
|
+
supplied[field] ??= 0
|
|
74
|
+
}
|
|
75
|
+
return supplied
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function assertExternalHeaderComplete(source: HeaderSource): Promise<void> {
|
|
79
|
+
const missing = missingHeaderFields(source)
|
|
80
|
+
if (!missing.length) return
|
|
81
|
+
const { translate } = await resolveTranslations()
|
|
82
|
+
throw new CrudHttpError(400, {
|
|
83
|
+
error: translate(
|
|
84
|
+
'sales.errors.externalTotalsRequired',
|
|
85
|
+
'An order with external amounts must supply its own document totals: {{fields}}.',
|
|
86
|
+
{ fields: missing.join(', ') },
|
|
87
|
+
),
|
|
88
|
+
})
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function assertExternalLineComplete(
|
|
92
|
+
line: Record<string, unknown>,
|
|
93
|
+
lineNumber: number,
|
|
94
|
+
): Promise<void> {
|
|
95
|
+
const missing = missingLineFields(line)
|
|
96
|
+
if (!missing.length) return
|
|
97
|
+
const { translate } = await resolveTranslations()
|
|
98
|
+
throw new CrudHttpError(400, {
|
|
99
|
+
error: translate(
|
|
100
|
+
'sales.errors.externalAmountsIncomplete',
|
|
101
|
+
'Line {{line}} has external amounts but is missing: {{fields}}. Partial specification is not a mode.',
|
|
102
|
+
{ line: String(lineNumber), fields: missing.join(', ') },
|
|
103
|
+
),
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function assertUniformAmountsMode(
|
|
108
|
+
documentMode: SalesAmountsMode,
|
|
109
|
+
lines: Array<{ amountsMode?: SalesAmountsMode | null }>,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const conflicting = lines.some(
|
|
112
|
+
(line) => line.amountsMode != null && line.amountsMode !== documentMode,
|
|
113
|
+
)
|
|
114
|
+
if (!conflicting) return
|
|
115
|
+
const { translate } = await resolveTranslations()
|
|
116
|
+
throw new CrudHttpError(400, {
|
|
117
|
+
error: translate(
|
|
118
|
+
'sales.errors.externalModeMixed',
|
|
119
|
+
'An order and all of its lines must share the same amounts mode.',
|
|
120
|
+
{ mode: documentMode },
|
|
121
|
+
),
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function refuseOnExternalOrder(): Promise<never> {
|
|
126
|
+
const { translate } = await resolveTranslations()
|
|
127
|
+
throw new CrudHttpError(409, {
|
|
128
|
+
error: translate(
|
|
129
|
+
'sales.errors.externalAdjustmentRefused',
|
|
130
|
+
'This order carries amounts from an external system, so adjustments cannot be added or removed. Switch it to computed amounts first.',
|
|
131
|
+
),
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export async function requireOrderTotalsForExternalWrite(source: HeaderSource): Promise<void> {
|
|
136
|
+
if (!hasAnySuppliedHeaderTotal(source)) {
|
|
137
|
+
const { translate } = await resolveTranslations()
|
|
138
|
+
throw new CrudHttpError(400, {
|
|
139
|
+
error: translate(
|
|
140
|
+
'sales.errors.externalTotalsRequired',
|
|
141
|
+
'An order with external amounts must supply its own document totals: {{fields}}.',
|
|
142
|
+
{ fields: REQUIRED_EXTERNAL_HEADER_FIELDS.join(', ') },
|
|
143
|
+
),
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
await assertExternalHeaderComplete(source)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function readPersistedHeaderTotals(
|
|
150
|
+
order: Record<ExternalHeaderField, string | number | null | undefined>,
|
|
151
|
+
): Partial<SalesDocumentAmounts> {
|
|
152
|
+
const totals: Partial<SalesDocumentAmounts> = {}
|
|
153
|
+
for (const field of EXTERNAL_HEADER_FIELDS) {
|
|
154
|
+
totals[field] = toFiniteNumber(order[field]) ?? 0
|
|
155
|
+
}
|
|
156
|
+
return totals
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function assertAmountsModeUnsupportedOnQuote(
|
|
160
|
+
payloads:
|
|
161
|
+
| Array<{ amountsMode?: SalesAmountsMode | null; totalsMode?: SalesAmountsMode | null }>
|
|
162
|
+
| null
|
|
163
|
+
| undefined,
|
|
164
|
+
): Promise<void> {
|
|
165
|
+
// Both reach quote commands, through schemas shared with orders.
|
|
166
|
+
if (!payloads?.some((payload) => payload.amountsMode != null || payload.totalsMode != null)) return
|
|
167
|
+
const { translate } = await resolveTranslations()
|
|
168
|
+
throw new CrudHttpError(400, {
|
|
169
|
+
error: translate(
|
|
170
|
+
'sales.errors.externalModeUnsupportedOnQuote',
|
|
171
|
+
'Quotes cannot carry an amounts mode; they always use computed amounts.',
|
|
172
|
+
),
|
|
173
|
+
})
|
|
174
|
+
}
|
|
@@ -80,7 +80,12 @@ function mapPersistedLine(line: SalesOrderLine | SalesQuoteLine): SalesPersisted
|
|
|
80
80
|
* the return flows kept the discount defect after the order flows were fixed.
|
|
81
81
|
*/
|
|
82
82
|
export function mapOrderLineEntityToSnapshot(line: SalesOrderLine): SalesPersistedLineSnapshot {
|
|
83
|
-
return
|
|
83
|
+
return {
|
|
84
|
+
...mapPersistedLine(line),
|
|
85
|
+
// Who owns the amounts, not where they came from — never conflate it with the
|
|
86
|
+
// origin flags above. Order-only: quote lines have no such column.
|
|
87
|
+
amountsMode: line.amountsMode ?? 'computed',
|
|
88
|
+
}
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
export function mapQuoteLineEntityToSnapshot(line: SalesQuoteLine): SalesPersistedLineSnapshot {
|
|
@@ -28,6 +28,8 @@ interface SalesLineRouteConfig {
|
|
|
28
28
|
parentFkColumn: string
|
|
29
29
|
parentFkParam: string
|
|
30
30
|
createSchema: z.ZodObject<z.ZodRawShape>
|
|
31
|
+
/** Extra fields accepted by the write payloads on top of `createSchema`. */
|
|
32
|
+
writeExtensionShape?: z.ZodRawShape
|
|
31
33
|
features: { view: string; manage: string }
|
|
32
34
|
commandPrefix: string
|
|
33
35
|
openApi: {
|
|
@@ -110,6 +112,7 @@ export function makeSalesLineRoute(config: SalesLineRouteConfig) {
|
|
|
110
112
|
parentFkColumn,
|
|
111
113
|
parentFkParam,
|
|
112
114
|
createSchema,
|
|
115
|
+
writeExtensionShape,
|
|
113
116
|
features,
|
|
114
117
|
commandPrefix,
|
|
115
118
|
} = config
|
|
@@ -127,11 +130,13 @@ export function makeSalesLineRoute(config: SalesLineRouteConfig) {
|
|
|
127
130
|
|
|
128
131
|
const upsertSchema = createSchema.extend({
|
|
129
132
|
id: z.string().uuid().optional(),
|
|
133
|
+
...(writeExtensionShape ?? {}),
|
|
130
134
|
})
|
|
131
135
|
|
|
132
136
|
const deleteSchema = z.object({
|
|
133
137
|
id: z.string().uuid(),
|
|
134
138
|
[parentFkParam]: z.string().uuid(),
|
|
139
|
+
...(writeExtensionShape ?? {}),
|
|
135
140
|
})
|
|
136
141
|
|
|
137
142
|
const routeMetadata = {
|
|
@@ -199,6 +204,8 @@ export function makeSalesLineRoute(config: SalesLineRouteConfig) {
|
|
|
199
204
|
]
|
|
200
205
|
const returnedQuantity = F['returned_quantity']
|
|
201
206
|
if (typeof returnedQuantity === 'string') fields.push(returnedQuantity)
|
|
207
|
+
const amountsMode = F['amounts_mode']
|
|
208
|
+
if (typeof amountsMode === 'string') fields.push(amountsMode)
|
|
202
209
|
return fields
|
|
203
210
|
})(),
|
|
204
211
|
sortFieldMap: {
|
|
@@ -321,6 +328,7 @@ export function makeSalesLineRoute(config: SalesLineRouteConfig) {
|
|
|
321
328
|
promotion_snapshot: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
322
329
|
metadata: z.record(z.string(), z.unknown()).nullable().optional(),
|
|
323
330
|
custom_field_set_id: z.string().uuid().nullable().optional(),
|
|
331
|
+
amounts_mode: z.enum(['computed', 'external']).optional(),
|
|
324
332
|
created_at: z.string(),
|
|
325
333
|
updated_at: z.string(),
|
|
326
334
|
})
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { registerSalesTotalsCalculator, rebuildDocumentResult } from '../calculations'
|
|
1
|
+
import { isExternalAmountsMode, registerSalesTotalsCalculator, rebuildDocumentResult } from '../calculations'
|
|
2
2
|
import type { SalesAdjustmentDraft, SalesDocumentCalculationResult } from '../types'
|
|
3
3
|
import {
|
|
4
4
|
getPaymentProvider,
|
|
@@ -180,7 +180,10 @@ export function ensureProviderTotalsCalculator() {
|
|
|
180
180
|
if (totalsRegistered) return
|
|
181
181
|
totalsRegistered = true
|
|
182
182
|
|
|
183
|
-
registerSalesTotalsCalculator(async ({ documentKind, lines, context, current, eventBus }) => {
|
|
183
|
+
registerSalesTotalsCalculator(async ({ documentKind, lines, context, current, eventBus, totalsMode }) => {
|
|
184
|
+
// A provider charge here could not move the caller's header, so it would show
|
|
185
|
+
// in the breakdown and be absent from the total.
|
|
186
|
+
if (isExternalAmountsMode(totalsMode)) return current
|
|
184
187
|
const metadata = (context.metadata ?? {}) as Record<string, unknown>
|
|
185
188
|
const shippingMethod = (metadata.shippingMethod ?? null) as ShippingMethodContext | null
|
|
186
189
|
const paymentMethod = (metadata.paymentMethod ?? null) as PaymentMethodContext | null
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import type { EventBus } from '@open-mercato/events'
|
|
2
2
|
import type { ReferenceUnitCode } from '@open-mercato/shared/lib/units/unitCodes'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
SalesAdjustmentKind,
|
|
5
|
+
SalesAmountsMode,
|
|
6
|
+
SalesDocumentKind,
|
|
7
|
+
SalesLineKind,
|
|
8
|
+
} from '../data/entities'
|
|
4
9
|
|
|
5
|
-
|
|
10
|
+
/**
|
|
11
|
+
* `SalesAmountsMode`: `computed` means core derives the amounts; `external` means
|
|
12
|
+
* they are the caller's, mirrored from another system, and are never recomputed.
|
|
13
|
+
*/
|
|
14
|
+
export type { SalesAdjustmentKind, SalesAmountsMode, SalesDocumentKind, SalesLineKind }
|
|
6
15
|
|
|
7
16
|
export type NumericLike = number | string
|
|
8
17
|
|
|
@@ -89,6 +98,8 @@ export type SalesLineSnapshot = {
|
|
|
89
98
|
metadata?: Record<string, unknown> | null
|
|
90
99
|
customFieldSetId?: string | null
|
|
91
100
|
customFields?: Record<string, unknown> | null
|
|
101
|
+
/** `external`: net, gross and tax are returned verbatim, never derived. Omitted means `computed`. */
|
|
102
|
+
amountsMode?: SalesAmountsMode | null
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
export type SalesAdjustmentDraft = {
|
|
@@ -155,6 +166,8 @@ export type SalesTotalsCalculationHook = (params: {
|
|
|
155
166
|
context: SalesCalculationContext
|
|
156
167
|
current: SalesDocumentCalculationResult
|
|
157
168
|
eventBus?: EventBus | null
|
|
169
|
+
/** `external`: the supplied header is re-applied after every calculator, so none can move it. */
|
|
170
|
+
totalsMode?: SalesAmountsMode | null
|
|
158
171
|
}) => SalesDocumentCalculationResult | Promise<SalesDocumentCalculationResult>
|
|
159
172
|
|
|
160
173
|
export type SalesCalculationContext = {
|
|
@@ -182,4 +195,8 @@ export type CalculateDocumentOptions = {
|
|
|
182
195
|
refundedTotalAmount?: number | null
|
|
183
196
|
}
|
|
184
197
|
eventBus?: EventBus | null
|
|
198
|
+
/** `external` makes {@link CalculateDocumentOptions.suppliedTotals} authoritative for the header. */
|
|
199
|
+
totalsMode?: SalesAmountsMode | null
|
|
200
|
+
/** The caller-asserted header, honoured verbatim when `totalsMode` is `external`. */
|
|
201
|
+
suppliedTotals?: Partial<SalesDocumentAmounts> | null
|
|
185
202
|
}
|
|
@@ -4222,6 +4222,22 @@
|
|
|
4222
4222
|
"name": "sales_order_lines",
|
|
4223
4223
|
"schema": "public",
|
|
4224
4224
|
"columns": {
|
|
4225
|
+
"amounts_mode": {
|
|
4226
|
+
"name": "amounts_mode",
|
|
4227
|
+
"type": "text",
|
|
4228
|
+
"unsigned": false,
|
|
4229
|
+
"autoincrement": false,
|
|
4230
|
+
"primary": false,
|
|
4231
|
+
"nullable": false,
|
|
4232
|
+
"unique": false,
|
|
4233
|
+
"length": null,
|
|
4234
|
+
"precision": null,
|
|
4235
|
+
"scale": null,
|
|
4236
|
+
"default": "'computed'",
|
|
4237
|
+
"comment": null,
|
|
4238
|
+
"enumItems": [],
|
|
4239
|
+
"mappedType": "text"
|
|
4240
|
+
},
|
|
4225
4241
|
"catalog_snapshot": {
|
|
4226
4242
|
"name": "catalog_snapshot",
|
|
4227
4243
|
"type": "jsonb",
|
|
@@ -5892,6 +5908,22 @@
|
|
|
5892
5908
|
"enumItems": [],
|
|
5893
5909
|
"mappedType": "uuid"
|
|
5894
5910
|
},
|
|
5911
|
+
"totals_mode": {
|
|
5912
|
+
"name": "totals_mode",
|
|
5913
|
+
"type": "text",
|
|
5914
|
+
"unsigned": false,
|
|
5915
|
+
"autoincrement": false,
|
|
5916
|
+
"primary": false,
|
|
5917
|
+
"nullable": false,
|
|
5918
|
+
"unique": false,
|
|
5919
|
+
"length": null,
|
|
5920
|
+
"precision": null,
|
|
5921
|
+
"scale": null,
|
|
5922
|
+
"default": "'computed'",
|
|
5923
|
+
"comment": null,
|
|
5924
|
+
"enumItems": [],
|
|
5925
|
+
"mappedType": "text"
|
|
5926
|
+
},
|
|
5895
5927
|
"totals_snapshot": {
|
|
5896
5928
|
"name": "totals_snapshot",
|
|
5897
5929
|
"type": "jsonb",
|
|
@@ -10943,4 +10975,4 @@
|
|
|
10943
10975
|
],
|
|
10944
10976
|
"views": [],
|
|
10945
10977
|
"nativeEnums": {}
|
|
10946
|
-
}
|
|
10978
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
|
|
3
|
+
// Opt-in `external` amounts mode for sales orders: the row-level statement that
|
|
4
|
+
// the stored amounts are a caller's assertion mirrored from an external book of
|
|
5
|
+
// record, not something core derived. Two defaulted columns, no backfill — every
|
|
6
|
+
// existing row takes 'computed', which is exactly today's behaviour, so the
|
|
7
|
+
// change is observably inert until a caller sets the mode.
|
|
8
|
+
//
|
|
9
|
+
// PostgreSQL has not rewritten a table for a defaulted column add since 11, so
|
|
10
|
+
// both statements are metadata-only regardless of table size.
|
|
11
|
+
export class Migration20260910120000_sales_external_amounts_mode extends Migration {
|
|
12
|
+
override async up(): Promise<void> {
|
|
13
|
+
this.addSql(
|
|
14
|
+
`alter table "sales_orders" add column if not exists "totals_mode" text not null default 'computed';`,
|
|
15
|
+
);
|
|
16
|
+
this.addSql(
|
|
17
|
+
`alter table "sales_order_lines" add column if not exists "amounts_mode" text not null default 'computed';`,
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
override async down(): Promise<void> {
|
|
22
|
+
this.addSql(`alter table "sales_order_lines" drop column if exists "amounts_mode";`);
|
|
23
|
+
this.addSql(`alter table "sales_orders" drop column if exists "totals_mode";`);
|
|
24
|
+
}
|
|
25
|
+
}
|