@open-mercato/core 0.9.1-develop.7532.1.8eac7e198f → 0.9.1-develop.7536.1.f9a5776a07
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/modules/auth/api/logout.js +8 -5
- package/dist/modules/auth/api/logout.js.map +2 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js +5 -2
- package/dist/modules/currencies/backend/currencies/[id]/page.js.map +2 -2
- package/dist/modules/currencies/commands/currencies.js +399 -137
- package/dist/modules/currencies/commands/currencies.js.map +2 -2
- package/dist/modules/currencies/lib/seeds.js +2 -0
- package/dist/modules/currencies/lib/seeds.js.map +2 -2
- package/dist/modules/customers/commands/companies.js +10 -3
- package/dist/modules/customers/commands/companies.js.map +2 -2
- package/dist/modules/customers/commands/interactionRestoreDetails.js +75 -0
- package/dist/modules/customers/commands/interactionRestoreDetails.js.map +7 -0
- package/dist/modules/customers/commands/people.js +10 -3
- package/dist/modules/customers/commands/people.js.map +2 -2
- package/package.json +7 -7
- package/src/modules/auth/api/logout.ts +9 -5
- package/src/modules/currencies/backend/currencies/[id]/page.tsx +5 -1
- package/src/modules/currencies/commands/currencies.ts +475 -168
- package/src/modules/currencies/i18n/de.json +4 -0
- package/src/modules/currencies/i18n/en.json +4 -0
- package/src/modules/currencies/i18n/es.json +4 -0
- package/src/modules/currencies/i18n/ko.json +4 -0
- package/src/modules/currencies/i18n/pl.json +4 -0
- package/src/modules/currencies/lib/seeds.ts +2 -0
- package/src/modules/customers/commands/companies.ts +16 -4
- package/src/modules/customers/commands/interactionRestoreDetails.ts +110 -0
- package/src/modules/customers/commands/people.ts +16 -4
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { registerCommand } from "@open-mercato/shared/lib/commands";
|
|
2
2
|
import { buildChanges, requireId, emitCrudSideEffects } from "@open-mercato/shared/lib/commands/helpers";
|
|
3
3
|
import { extractUndoPayload } from "@open-mercato/shared/lib/commands/undo";
|
|
4
|
-
import {
|
|
4
|
+
import { resolveRedoSnapshot, restoreCreatedRow, reviveSnapshotSeed } from "@open-mercato/shared/lib/commands/redo";
|
|
5
5
|
import { withAtomicFlush } from "@open-mercato/shared/lib/commands/flush";
|
|
6
6
|
import { CrudHttpError, conflict, isUniqueViolation } from "@open-mercato/shared/lib/crud/errors";
|
|
7
7
|
import { resolveTranslations } from "@open-mercato/shared/lib/i18n/server";
|
|
@@ -45,55 +45,159 @@ async function loadCurrencySnapshot(em, id, ctx) {
|
|
|
45
45
|
updatedAt: record.updatedAt.toISOString()
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const NO_BASE_TRANSITION = { promoted: false, demotedBaseCurrencyIds: [] };
|
|
49
|
+
const BASE_CURRENCY_LOCK_SQL = "select pg_advisory_xact_lock(hashtextextended(?, 0))";
|
|
50
|
+
const UPDATABLE_CURRENCY_FIELDS = [
|
|
51
|
+
"code",
|
|
52
|
+
"name",
|
|
53
|
+
"symbol",
|
|
54
|
+
"decimalPlaces",
|
|
55
|
+
"thousandsSeparator",
|
|
56
|
+
"decimalSeparator",
|
|
57
|
+
"isBase",
|
|
58
|
+
"isActive"
|
|
59
|
+
];
|
|
60
|
+
function baseCurrencyLockKey(scope) {
|
|
61
|
+
return `currencies:base:${scope.tenantId}:${scope.organizationId}`;
|
|
62
|
+
}
|
|
63
|
+
async function lockBaseCurrencyScope(em, scope) {
|
|
64
|
+
if (!em.isInTransaction()) {
|
|
65
|
+
throw new Error("[internal] Currency base lock requires an active transaction");
|
|
66
|
+
}
|
|
67
|
+
await em.execute(BASE_CURRENCY_LOCK_SQL, [baseCurrencyLockKey(scope)]);
|
|
68
|
+
}
|
|
69
|
+
async function findLiveBaseCurrencyIds(em, scope, excludeId) {
|
|
70
|
+
const rows = await em.find(
|
|
50
71
|
Currency,
|
|
51
72
|
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
id: { $ne: currencyId },
|
|
73
|
+
tenantId: scope.tenantId,
|
|
74
|
+
organizationId: scope.organizationId,
|
|
55
75
|
isBase: true,
|
|
56
|
-
deletedAt: null
|
|
76
|
+
deletedAt: null,
|
|
77
|
+
...excludeId ? { id: { $ne: excludeId } } : {}
|
|
78
|
+
},
|
|
79
|
+
{ orderBy: { createdAt: "asc", id: "asc" } }
|
|
80
|
+
);
|
|
81
|
+
return rows.map((row) => row.id);
|
|
82
|
+
}
|
|
83
|
+
async function setBaseFlag(em, scope, ids, isBase) {
|
|
84
|
+
if (ids.length === 0) return;
|
|
85
|
+
await em.nativeUpdate(
|
|
86
|
+
Currency,
|
|
87
|
+
{
|
|
88
|
+
tenantId: scope.tenantId,
|
|
89
|
+
organizationId: scope.organizationId,
|
|
90
|
+
id: { $in: ids }
|
|
57
91
|
},
|
|
58
|
-
{ isBase
|
|
92
|
+
{ isBase, updatedAt: /* @__PURE__ */ new Date() }
|
|
59
93
|
);
|
|
60
94
|
}
|
|
95
|
+
async function findFirstLivePredecessorId(em, scope, candidateIds) {
|
|
96
|
+
if (candidateIds.length === 0) return null;
|
|
97
|
+
const rows = await em.find(Currency, {
|
|
98
|
+
tenantId: scope.tenantId,
|
|
99
|
+
organizationId: scope.organizationId,
|
|
100
|
+
id: { $in: candidateIds },
|
|
101
|
+
deletedAt: null
|
|
102
|
+
});
|
|
103
|
+
const liveIds = new Set(rows.map((row) => row.id));
|
|
104
|
+
return candidateIds.find((id) => liveIds.has(id)) ?? null;
|
|
105
|
+
}
|
|
106
|
+
async function translatedHttpError(status, key, fallback) {
|
|
107
|
+
const { translate } = await resolveTranslations();
|
|
108
|
+
return new CrudHttpError(status, { error: translate(key, fallback) });
|
|
109
|
+
}
|
|
110
|
+
async function resolvePredecessorOrRefuse(em, scope, candidateIds) {
|
|
111
|
+
const predecessorId = await findFirstLivePredecessorId(em, scope, candidateIds);
|
|
112
|
+
if (!predecessorId) {
|
|
113
|
+
throw await translatedHttpError(
|
|
114
|
+
409,
|
|
115
|
+
"currencies.errors.undoWouldClearBase",
|
|
116
|
+
"This change can't be undone because it would leave the organization without a base currency."
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return predecessorId;
|
|
120
|
+
}
|
|
121
|
+
function countActiveRatesForCode(em, scope, code) {
|
|
122
|
+
return em.count(ExchangeRate, {
|
|
123
|
+
$or: [
|
|
124
|
+
{ fromCurrencyCode: code },
|
|
125
|
+
{ toCurrencyCode: code }
|
|
126
|
+
],
|
|
127
|
+
organizationId: scope.organizationId,
|
|
128
|
+
tenantId: scope.tenantId,
|
|
129
|
+
deletedAt: null,
|
|
130
|
+
isActive: true
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
function readScopedSnapshotRecord(em, snapshot) {
|
|
134
|
+
return em.findOne(Currency, {
|
|
135
|
+
id: snapshot.id,
|
|
136
|
+
tenantId: snapshot.tenantId,
|
|
137
|
+
organizationId: snapshot.organizationId
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
function scopeOf(record) {
|
|
141
|
+
return { tenantId: record.tenantId, organizationId: record.organizationId };
|
|
142
|
+
}
|
|
143
|
+
function omitIsBase(input) {
|
|
144
|
+
const rest = { ...input };
|
|
145
|
+
delete rest.isBase;
|
|
146
|
+
return rest;
|
|
147
|
+
}
|
|
148
|
+
function isPromotionByLegacyLog(payload) {
|
|
149
|
+
return payload?.before?.isBase === false && payload?.after?.isBase === true;
|
|
150
|
+
}
|
|
151
|
+
function isDemotionByLegacyLog(payload) {
|
|
152
|
+
return !payload?.base && payload?.before?.isBase === true && payload?.after?.isBase === false;
|
|
153
|
+
}
|
|
61
154
|
const createCurrencyCommand = {
|
|
62
155
|
id: "currencies.currencies.create",
|
|
63
156
|
async execute(input, ctx) {
|
|
64
157
|
const parsed = currencyCreateSchema.parse(input);
|
|
65
158
|
ensureCurrencyCommandScope(ctx, parsed);
|
|
66
159
|
const em = ctx.container.resolve("em").fork();
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
tenantId: parsed.tenantId,
|
|
71
|
-
deletedAt: null
|
|
72
|
-
});
|
|
73
|
-
if (existing) {
|
|
74
|
-
throw conflict("Currency code already exists for this organization.");
|
|
75
|
-
}
|
|
76
|
-
const now = /* @__PURE__ */ new Date();
|
|
77
|
-
const record = em.create(Currency, {
|
|
78
|
-
organizationId: parsed.organizationId,
|
|
79
|
-
tenantId: parsed.tenantId,
|
|
80
|
-
code: parsed.code,
|
|
81
|
-
name: parsed.name,
|
|
82
|
-
symbol: parsed.symbol ?? null,
|
|
83
|
-
decimalPlaces: parsed.decimalPlaces ?? 2,
|
|
84
|
-
thousandsSeparator: parsed.thousandsSeparator ?? null,
|
|
85
|
-
decimalSeparator: parsed.decimalSeparator ?? null,
|
|
86
|
-
isBase: parsed.isBase ?? false,
|
|
87
|
-
isActive: parsed.isActive !== false,
|
|
88
|
-
createdAt: now,
|
|
89
|
-
updatedAt: now
|
|
90
|
-
});
|
|
91
|
-
em.persist(record);
|
|
160
|
+
const scope = scopeOf(parsed);
|
|
161
|
+
let demotedBaseCurrencyIds = [];
|
|
162
|
+
let record;
|
|
92
163
|
try {
|
|
93
164
|
await withAtomicFlush(
|
|
94
165
|
em,
|
|
95
166
|
[
|
|
96
|
-
() =>
|
|
167
|
+
async () => {
|
|
168
|
+
await lockBaseCurrencyScope(em, scope);
|
|
169
|
+
const existing = await em.findOne(Currency, {
|
|
170
|
+
code: parsed.code,
|
|
171
|
+
organizationId: parsed.organizationId,
|
|
172
|
+
tenantId: parsed.tenantId,
|
|
173
|
+
deletedAt: null
|
|
174
|
+
});
|
|
175
|
+
if (existing) {
|
|
176
|
+
throw conflict("Currency code already exists for this organization.");
|
|
177
|
+
}
|
|
178
|
+
if (parsed.isBase) {
|
|
179
|
+
demotedBaseCurrencyIds = await findLiveBaseCurrencyIds(em, scope);
|
|
180
|
+
await setBaseFlag(em, scope, demotedBaseCurrencyIds, false);
|
|
181
|
+
}
|
|
182
|
+
},
|
|
183
|
+
() => {
|
|
184
|
+
const now = /* @__PURE__ */ new Date();
|
|
185
|
+
record = em.create(Currency, {
|
|
186
|
+
organizationId: parsed.organizationId,
|
|
187
|
+
tenantId: parsed.tenantId,
|
|
188
|
+
code: parsed.code,
|
|
189
|
+
name: parsed.name,
|
|
190
|
+
symbol: parsed.symbol ?? null,
|
|
191
|
+
decimalPlaces: parsed.decimalPlaces ?? 2,
|
|
192
|
+
thousandsSeparator: parsed.thousandsSeparator ?? null,
|
|
193
|
+
decimalSeparator: parsed.decimalSeparator ?? null,
|
|
194
|
+
isBase: parsed.isBase ?? false,
|
|
195
|
+
isActive: parsed.isActive !== false,
|
|
196
|
+
createdAt: now,
|
|
197
|
+
updatedAt: now
|
|
198
|
+
});
|
|
199
|
+
em.persist(record);
|
|
200
|
+
}
|
|
97
201
|
],
|
|
98
202
|
{ transaction: true }
|
|
99
203
|
);
|
|
@@ -115,13 +219,13 @@ const createCurrencyCommand = {
|
|
|
115
219
|
},
|
|
116
220
|
events: currencyCrudEvents
|
|
117
221
|
});
|
|
118
|
-
return { currencyId: record.id };
|
|
222
|
+
return record.isBase ? { currencyId: record.id, base: { promoted: true, demotedBaseCurrencyIds } } : { currencyId: record.id };
|
|
119
223
|
},
|
|
120
224
|
captureAfter: async (_input, result, ctx) => {
|
|
121
225
|
const em = ctx.container.resolve("em").fork();
|
|
122
226
|
return loadCurrencySnapshot(em, result.currencyId, ctx);
|
|
123
227
|
},
|
|
124
|
-
buildLog: async ({ snapshots }) => {
|
|
228
|
+
buildLog: async ({ snapshots, result }) => {
|
|
125
229
|
const after = snapshots.after;
|
|
126
230
|
if (!after) return null;
|
|
127
231
|
const { translate } = await resolveTranslations();
|
|
@@ -132,31 +236,104 @@ const createCurrencyCommand = {
|
|
|
132
236
|
tenantId: after.tenantId,
|
|
133
237
|
organizationId: after.organizationId,
|
|
134
238
|
snapshotAfter: after,
|
|
135
|
-
payload: { undo: { after } }
|
|
239
|
+
payload: { undo: { after, base: result?.base ?? NO_BASE_TRANSITION } }
|
|
136
240
|
};
|
|
137
241
|
},
|
|
138
242
|
undo: async ({ logEntry, ctx }) => {
|
|
139
243
|
const payload = extractUndoPayload(logEntry);
|
|
140
244
|
const after = payload?.after ?? null;
|
|
141
245
|
if (!after) return;
|
|
246
|
+
const scope = scopeOf(after);
|
|
247
|
+
const promoted = payload?.base ? payload.base.promoted : after.isBase;
|
|
248
|
+
const predecessorIds = payload?.base?.demotedBaseCurrencyIds ?? [];
|
|
142
249
|
const em = ctx.container.resolve("em").fork();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
250
|
+
let record = null;
|
|
251
|
+
let restoredBaseId = null;
|
|
252
|
+
await withAtomicFlush(
|
|
253
|
+
em,
|
|
254
|
+
[
|
|
255
|
+
async () => {
|
|
256
|
+
await lockBaseCurrencyScope(em, scope);
|
|
257
|
+
const current = await readScopedSnapshotRecord(em, after);
|
|
258
|
+
if (!current || current.deletedAt) return;
|
|
259
|
+
const activeRatesCount = await countActiveRatesForCode(em, scope, current.code);
|
|
260
|
+
if (activeRatesCount > 0) {
|
|
261
|
+
throw await translatedHttpError(
|
|
262
|
+
409,
|
|
263
|
+
"currencies.errors.undoHasRates",
|
|
264
|
+
"This change can't be undone because exchange rates now use this currency. Delete or deactivate those exchange rates first."
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
if (current.isBase) {
|
|
268
|
+
restoredBaseId = await resolvePredecessorOrRefuse(em, scope, promoted ? predecessorIds : []);
|
|
269
|
+
}
|
|
270
|
+
record = current;
|
|
271
|
+
},
|
|
272
|
+
() => {
|
|
273
|
+
if (!record) return;
|
|
274
|
+
record.deletedAt = /* @__PURE__ */ new Date();
|
|
275
|
+
record.isActive = false;
|
|
276
|
+
record.isBase = false;
|
|
277
|
+
},
|
|
278
|
+
() => restoredBaseId ? setBaseFlag(em, scope, [restoredBaseId], true) : void 0
|
|
279
|
+
],
|
|
280
|
+
{ transaction: true }
|
|
281
|
+
);
|
|
148
282
|
},
|
|
149
|
-
redo:
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
283
|
+
redo: async ({ ctx, logEntry }) => {
|
|
284
|
+
const snapshot = resolveRedoSnapshot(logEntry);
|
|
285
|
+
if (!snapshot || !snapshot.id) {
|
|
286
|
+
throw new CrudHttpError(400, { error: "[internal] redo snapshot unavailable for create command" });
|
|
287
|
+
}
|
|
288
|
+
const scope = scopeOf(snapshot);
|
|
289
|
+
const isBase = snapshot.isBase === true;
|
|
290
|
+
const em = ctx.container.resolve("em").fork();
|
|
291
|
+
let demotedBaseCurrencyIds = [];
|
|
292
|
+
let record;
|
|
293
|
+
try {
|
|
294
|
+
await withAtomicFlush(
|
|
295
|
+
em,
|
|
296
|
+
[
|
|
297
|
+
async () => {
|
|
298
|
+
await lockBaseCurrencyScope(em, scope);
|
|
299
|
+
if (isBase) {
|
|
300
|
+
demotedBaseCurrencyIds = await findLiveBaseCurrencyIds(em, scope, snapshot.id);
|
|
301
|
+
await setBaseFlag(em, scope, demotedBaseCurrencyIds, false);
|
|
302
|
+
}
|
|
303
|
+
},
|
|
304
|
+
async () => {
|
|
305
|
+
record = await restoreCreatedRow(
|
|
306
|
+
em,
|
|
307
|
+
Currency,
|
|
308
|
+
snapshot.id,
|
|
309
|
+
() => reviveSnapshotSeed(snapshot),
|
|
310
|
+
(lookupEm) => readScopedSnapshotRecord(lookupEm, snapshot)
|
|
311
|
+
);
|
|
312
|
+
record.isBase = isBase;
|
|
313
|
+
}
|
|
314
|
+
],
|
|
315
|
+
{ transaction: true }
|
|
316
|
+
);
|
|
317
|
+
} catch (err) {
|
|
318
|
+
if (isUniqueViolation(err)) {
|
|
319
|
+
throw conflict("[internal] Cannot redo: a record with the same unique key already exists.");
|
|
157
320
|
}
|
|
321
|
+
throw err;
|
|
158
322
|
}
|
|
159
|
-
|
|
323
|
+
const de = ctx.container.resolve("dataEngine");
|
|
324
|
+
await emitCrudSideEffects({
|
|
325
|
+
dataEngine: de,
|
|
326
|
+
action: "created",
|
|
327
|
+
entity: record,
|
|
328
|
+
identifiers: {
|
|
329
|
+
id: record.id,
|
|
330
|
+
organizationId: record.organizationId,
|
|
331
|
+
tenantId: record.tenantId
|
|
332
|
+
},
|
|
333
|
+
events: currencyCrudEvents
|
|
334
|
+
});
|
|
335
|
+
return isBase ? { currencyId: record.id, base: { promoted: true, demotedBaseCurrencyIds } } : { currencyId: record.id };
|
|
336
|
+
}
|
|
160
337
|
};
|
|
161
338
|
const updateCurrencyCommand = {
|
|
162
339
|
id: "currencies.currencies.update",
|
|
@@ -169,57 +346,81 @@ const updateCurrencyCommand = {
|
|
|
169
346
|
async execute(input, ctx) {
|
|
170
347
|
const parsed = currencyUpdateSchema.parse(input);
|
|
171
348
|
requireId(parsed.id, "Currency ID is required");
|
|
172
|
-
const
|
|
173
|
-
const record = await em.findOne(
|
|
349
|
+
const scopeRecord = await ctx.container.resolve("em").fork().findOne(
|
|
174
350
|
Currency,
|
|
175
351
|
buildCurrencyCommandWhere(ctx, { id: parsed.id })
|
|
176
352
|
);
|
|
177
|
-
if (!
|
|
353
|
+
if (!scopeRecord) {
|
|
178
354
|
throw new CrudHttpError(404, { error: "Currency not found" });
|
|
179
355
|
}
|
|
180
|
-
ensureCurrencyCommandScope(ctx,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
deletedAt: null
|
|
188
|
-
});
|
|
189
|
-
if (existing) {
|
|
190
|
-
throw conflict("Currency code already exists for this organization.");
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
const allChanges = buildChanges(record, parsed, [
|
|
194
|
-
"code",
|
|
195
|
-
"name",
|
|
196
|
-
"symbol",
|
|
197
|
-
"decimalPlaces",
|
|
198
|
-
"thousandsSeparator",
|
|
199
|
-
"decimalSeparator",
|
|
200
|
-
"isBase",
|
|
201
|
-
"isActive"
|
|
202
|
-
]);
|
|
203
|
-
const changes = Object.fromEntries(
|
|
204
|
-
Object.entries(allChanges).filter(([, c]) => c.to !== void 0)
|
|
205
|
-
);
|
|
206
|
-
if (Object.keys(changes).length === 0) {
|
|
207
|
-
return { currencyId: record.id };
|
|
208
|
-
}
|
|
356
|
+
ensureCurrencyCommandScope(ctx, scopeRecord);
|
|
357
|
+
const scope = scopeOf(scopeRecord);
|
|
358
|
+
const em = ctx.container.resolve("em").fork();
|
|
359
|
+
let record;
|
|
360
|
+
let changes = {};
|
|
361
|
+
let promoted = false;
|
|
362
|
+
let demotedBaseCurrencyIds = [];
|
|
209
363
|
await withAtomicFlush(
|
|
210
364
|
em,
|
|
211
365
|
[
|
|
212
|
-
() => {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
366
|
+
async () => {
|
|
367
|
+
await lockBaseCurrencyScope(em, scope);
|
|
368
|
+
const current = await em.findOne(
|
|
369
|
+
Currency,
|
|
370
|
+
buildCurrencyCommandWhere(ctx, { id: parsed.id })
|
|
371
|
+
);
|
|
372
|
+
if (!current) {
|
|
373
|
+
throw new CrudHttpError(404, { error: "Currency not found" });
|
|
216
374
|
}
|
|
217
|
-
|
|
375
|
+
ensureCurrencyCommandScope(ctx, current);
|
|
376
|
+
record = current;
|
|
377
|
+
if (parsed.code && parsed.code !== record.code) {
|
|
378
|
+
const existing = await em.findOne(Currency, {
|
|
379
|
+
code: parsed.code,
|
|
380
|
+
organizationId: record.organizationId,
|
|
381
|
+
tenantId: record.tenantId,
|
|
382
|
+
id: { $ne: record.id },
|
|
383
|
+
deletedAt: null
|
|
384
|
+
});
|
|
385
|
+
if (existing) {
|
|
386
|
+
throw conflict("Currency code already exists for this organization.");
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (parsed.isBase === false && record.isBase) {
|
|
390
|
+
throw await translatedHttpError(
|
|
391
|
+
400,
|
|
392
|
+
"currencies.errors.baseCurrencyUnset",
|
|
393
|
+
"Choose another currency as the base instead of clearing it."
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
const allChanges = buildChanges(
|
|
397
|
+
record,
|
|
398
|
+
parsed,
|
|
399
|
+
UPDATABLE_CURRENCY_FIELDS
|
|
400
|
+
);
|
|
401
|
+
changes = Object.fromEntries(
|
|
402
|
+
Object.entries(allChanges).filter(([, change]) => change.to !== void 0)
|
|
403
|
+
);
|
|
404
|
+
promoted = changes.isBase?.to === true;
|
|
405
|
+
if (promoted) {
|
|
406
|
+
demotedBaseCurrencyIds = await findLiveBaseCurrencyIds(em, scope, record.id);
|
|
407
|
+
}
|
|
408
|
+
},
|
|
409
|
+
() => {
|
|
410
|
+
if (Object.keys(changes).length === 0) return;
|
|
411
|
+
const assignments = Object.fromEntries(
|
|
412
|
+
Object.entries(changes).map(([key, change]) => [key, change.to])
|
|
413
|
+
);
|
|
414
|
+
Object.assign(record, assignments, { updatedAt: /* @__PURE__ */ new Date() });
|
|
218
415
|
},
|
|
219
|
-
() =>
|
|
416
|
+
() => setBaseFlag(em, scope, demotedBaseCurrencyIds, false)
|
|
220
417
|
],
|
|
221
418
|
{ transaction: true }
|
|
222
419
|
);
|
|
420
|
+
const result = promoted ? { currencyId: record.id, base: { promoted: true, demotedBaseCurrencyIds } } : { currencyId: record.id };
|
|
421
|
+
if (Object.keys(changes).length === 0) {
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
223
424
|
const de = ctx.container.resolve("dataEngine");
|
|
224
425
|
await emitCrudSideEffects({
|
|
225
426
|
dataEngine: de,
|
|
@@ -232,17 +433,18 @@ const updateCurrencyCommand = {
|
|
|
232
433
|
},
|
|
233
434
|
events: currencyCrudEvents
|
|
234
435
|
});
|
|
235
|
-
return
|
|
436
|
+
return result;
|
|
236
437
|
},
|
|
237
438
|
captureAfter: async (_input, result, ctx) => {
|
|
238
439
|
const em = ctx.container.resolve("em").fork();
|
|
239
440
|
return loadCurrencySnapshot(em, result.currencyId, ctx);
|
|
240
441
|
},
|
|
241
|
-
buildLog: async ({ snapshots, result }) => {
|
|
442
|
+
buildLog: async ({ input, snapshots, result }) => {
|
|
242
443
|
const before = snapshots.before;
|
|
243
444
|
const after = snapshots.after;
|
|
244
445
|
if (!after) return null;
|
|
245
446
|
const { translate } = await resolveTranslations();
|
|
447
|
+
const base = result?.base ?? NO_BASE_TRANSITION;
|
|
246
448
|
return {
|
|
247
449
|
actionLabel: translate("currencies.audit.update", "Update currency"),
|
|
248
450
|
resourceKind: "currencies.currency",
|
|
@@ -251,28 +453,56 @@ const updateCurrencyCommand = {
|
|
|
251
453
|
organizationId: after.organizationId,
|
|
252
454
|
snapshotBefore: before ?? void 0,
|
|
253
455
|
snapshotAfter: after,
|
|
254
|
-
payload: { undo: { before, after } }
|
|
456
|
+
payload: { undo: { before, after, base } },
|
|
457
|
+
redoInput: base.promoted ? input : omitIsBase(input)
|
|
255
458
|
};
|
|
256
459
|
},
|
|
257
460
|
undo: async ({ logEntry, ctx }) => {
|
|
258
461
|
const payload = extractUndoPayload(logEntry);
|
|
259
462
|
const before = payload?.before ?? null;
|
|
260
463
|
if (!before) return;
|
|
464
|
+
const scope = scopeOf(before);
|
|
465
|
+
const promoted = payload?.base ? payload.base.promoted : isPromotionByLegacyLog(payload);
|
|
466
|
+
const predecessorIds = payload?.base?.demotedBaseCurrencyIds ?? [];
|
|
261
467
|
const em = ctx.container.resolve("em").fork();
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
468
|
+
const demotedByLegacyLog = isDemotionByLegacyLog(payload);
|
|
469
|
+
let record = null;
|
|
470
|
+
let restoredBaseId = null;
|
|
471
|
+
let repromote = false;
|
|
472
|
+
await withAtomicFlush(
|
|
473
|
+
em,
|
|
474
|
+
[
|
|
475
|
+
async () => {
|
|
476
|
+
await lockBaseCurrencyScope(em, scope);
|
|
477
|
+
const current = await readScopedSnapshotRecord(em, before);
|
|
478
|
+
if (!current) return;
|
|
479
|
+
if (promoted && current.isBase && !current.deletedAt) {
|
|
480
|
+
restoredBaseId = await resolvePredecessorOrRefuse(em, scope, predecessorIds);
|
|
481
|
+
}
|
|
482
|
+
if (demotedByLegacyLog && !current.isBase && !current.deletedAt) {
|
|
483
|
+
repromote = (await findLiveBaseCurrencyIds(em, scope)).length === 0;
|
|
484
|
+
}
|
|
485
|
+
record = current;
|
|
486
|
+
},
|
|
487
|
+
() => {
|
|
488
|
+
if (!record) return;
|
|
489
|
+
Object.assign(record, {
|
|
490
|
+
code: before.code,
|
|
491
|
+
name: before.name,
|
|
492
|
+
symbol: before.symbol,
|
|
493
|
+
decimalPlaces: before.decimalPlaces,
|
|
494
|
+
thousandsSeparator: before.thousandsSeparator,
|
|
495
|
+
decimalSeparator: before.decimalSeparator,
|
|
496
|
+
isActive: before.isActive,
|
|
497
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
498
|
+
});
|
|
499
|
+
if (restoredBaseId) record.isBase = false;
|
|
500
|
+
if (repromote) record.isBase = true;
|
|
501
|
+
},
|
|
502
|
+
() => restoredBaseId ? setBaseFlag(em, scope, [restoredBaseId], true) : void 0
|
|
503
|
+
],
|
|
504
|
+
{ transaction: true }
|
|
505
|
+
);
|
|
276
506
|
}
|
|
277
507
|
};
|
|
278
508
|
const deleteCurrencyCommand = {
|
|
@@ -286,36 +516,48 @@ const deleteCurrencyCommand = {
|
|
|
286
516
|
async execute(input, ctx) {
|
|
287
517
|
const parsed = currencyDeleteSchema.parse(input);
|
|
288
518
|
requireId(parsed.id, "Currency ID is required");
|
|
289
|
-
const
|
|
290
|
-
const record = await em.findOne(
|
|
519
|
+
const scopeRecord = await ctx.container.resolve("em").fork().findOne(
|
|
291
520
|
Currency,
|
|
292
521
|
buildCurrencyCommandWhere(ctx, { id: parsed.id })
|
|
293
522
|
);
|
|
294
|
-
if (!
|
|
523
|
+
if (!scopeRecord) {
|
|
295
524
|
throw new CrudHttpError(404, { error: "Currency not found" });
|
|
296
525
|
}
|
|
297
|
-
ensureCurrencyCommandScope(ctx,
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
526
|
+
ensureCurrencyCommandScope(ctx, scopeRecord);
|
|
527
|
+
const scope = scopeOf(scopeRecord);
|
|
528
|
+
const em = ctx.container.resolve("em").fork();
|
|
529
|
+
let record;
|
|
530
|
+
await withAtomicFlush(
|
|
531
|
+
em,
|
|
532
|
+
[
|
|
533
|
+
async () => {
|
|
534
|
+
await lockBaseCurrencyScope(em, scope);
|
|
535
|
+
const current = await em.findOne(
|
|
536
|
+
Currency,
|
|
537
|
+
buildCurrencyCommandWhere(ctx, { id: parsed.id })
|
|
538
|
+
);
|
|
539
|
+
if (!current) {
|
|
540
|
+
throw new CrudHttpError(404, { error: "Currency not found" });
|
|
541
|
+
}
|
|
542
|
+
ensureCurrencyCommandScope(ctx, current);
|
|
543
|
+
if (current.isBase) {
|
|
544
|
+
throw new CrudHttpError(400, { error: "Cannot delete the base currency" });
|
|
545
|
+
}
|
|
546
|
+
const activeRatesCount = await countActiveRatesForCode(em, scope, current.code);
|
|
547
|
+
if (activeRatesCount > 0) {
|
|
548
|
+
throw new CrudHttpError(400, {
|
|
549
|
+
error: `Cannot delete currency ${current.code} because it has ${activeRatesCount} active exchange rate(s). Please delete or deactivate the exchange rates first.`
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
record = current;
|
|
553
|
+
},
|
|
554
|
+
() => {
|
|
555
|
+
record.deletedAt = /* @__PURE__ */ new Date();
|
|
556
|
+
record.isActive = false;
|
|
557
|
+
}
|
|
305
558
|
],
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
deletedAt: null,
|
|
309
|
-
isActive: true
|
|
310
|
-
});
|
|
311
|
-
if (activeRatesCount > 0) {
|
|
312
|
-
throw new CrudHttpError(400, {
|
|
313
|
-
error: `Cannot delete currency ${record.code} because it has ${activeRatesCount} active exchange rate(s). Please delete or deactivate the exchange rates first.`
|
|
314
|
-
});
|
|
315
|
-
}
|
|
316
|
-
record.deletedAt = /* @__PURE__ */ new Date();
|
|
317
|
-
record.isActive = false;
|
|
318
|
-
await em.flush();
|
|
559
|
+
{ transaction: true }
|
|
560
|
+
);
|
|
319
561
|
const de = ctx.container.resolve("dataEngine");
|
|
320
562
|
await emitCrudSideEffects({
|
|
321
563
|
dataEngine: de,
|
|
@@ -330,7 +572,7 @@ const deleteCurrencyCommand = {
|
|
|
330
572
|
});
|
|
331
573
|
return { currencyId: record.id };
|
|
332
574
|
},
|
|
333
|
-
buildLog: async ({ snapshots
|
|
575
|
+
buildLog: async ({ snapshots }) => {
|
|
334
576
|
const before = snapshots.before;
|
|
335
577
|
if (!before) return null;
|
|
336
578
|
const { translate } = await resolveTranslations();
|
|
@@ -348,13 +590,33 @@ const deleteCurrencyCommand = {
|
|
|
348
590
|
const payload = extractUndoPayload(logEntry);
|
|
349
591
|
const before = payload?.before ?? null;
|
|
350
592
|
if (!before) return;
|
|
593
|
+
const scope = scopeOf(before);
|
|
351
594
|
const em = ctx.container.resolve("em").fork();
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
595
|
+
let record = null;
|
|
596
|
+
let reviveAsNonBase = false;
|
|
597
|
+
await withAtomicFlush(
|
|
598
|
+
em,
|
|
599
|
+
[
|
|
600
|
+
async () => {
|
|
601
|
+
await lockBaseCurrencyScope(em, scope);
|
|
602
|
+
const current = await readScopedSnapshotRecord(em, before);
|
|
603
|
+
if (!current) return;
|
|
604
|
+
if (current.isBase) {
|
|
605
|
+
const otherBaseIds = await findLiveBaseCurrencyIds(em, scope, current.id);
|
|
606
|
+
reviveAsNonBase = otherBaseIds.length > 0;
|
|
607
|
+
}
|
|
608
|
+
record = current;
|
|
609
|
+
},
|
|
610
|
+
() => {
|
|
611
|
+
if (!record) return;
|
|
612
|
+
record.deletedAt = null;
|
|
613
|
+
record.isActive = before.isActive;
|
|
614
|
+
if (reviveAsNonBase) record.isBase = false;
|
|
615
|
+
record.updatedAt = /* @__PURE__ */ new Date();
|
|
616
|
+
}
|
|
617
|
+
],
|
|
618
|
+
{ transaction: true }
|
|
619
|
+
);
|
|
358
620
|
}
|
|
359
621
|
};
|
|
360
622
|
registerCommand(createCurrencyCommand);
|