@revenexx/cover 0.1.16 → 0.1.18

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.
@@ -296,12 +296,35 @@ async function orderAgain(): Promise<void> {
296
296
  </span>
297
297
  </li>
298
298
  </ul>
299
- <div
300
- class="flex justify-between px-4 sm:px-5 py-3 border-t border-(--ui-border)
301
- text-sm font-semibold text-highlighted"
302
- >
303
- <span>{{ t('orders.card.total') }}</span>
304
- <span class="tabular-nums">{{ formatCurrency(order.total) }}</span>
299
+ <div class="px-4 sm:px-5 py-3 border-t border-(--ui-border) text-sm">
300
+ <div
301
+ v-if="order.subtotal !== undefined"
302
+ class="flex justify-between py-0.5 text-highlighted"
303
+ >
304
+ <span>{{ t('orders.card.subtotal') }}</span>
305
+ <span class="tabular-nums">{{ formatCurrency(order.subtotal) }}</span>
306
+ </div>
307
+ <div
308
+ v-if="order.shipping !== undefined"
309
+ class="flex justify-between py-0.5 text-highlighted"
310
+ >
311
+ <span>{{ t('orders.card.shipping') }}</span>
312
+ <span class="tabular-nums">{{ formatCurrency(order.shipping) }}</span>
313
+ </div>
314
+ <div
315
+ v-if="order.tax !== undefined"
316
+ class="flex justify-between py-0.5 text-muted"
317
+ >
318
+ <span>{{ t('orders.card.tax') }}</span>
319
+ <span class="tabular-nums">{{ formatCurrency(order.tax) }}</span>
320
+ </div>
321
+ <div
322
+ class="flex justify-between pt-2 mt-1 border-t border-(--ui-border)
323
+ font-semibold text-highlighted"
324
+ >
325
+ <span>{{ t('orders.card.total') }}</span>
326
+ <span class="tabular-nums">{{ formatCurrency(order.total) }}</span>
327
+ </div>
305
328
  </div>
306
329
  </section>
307
330
 
@@ -24,7 +24,13 @@ export interface AccountOrder {
24
24
  readonly date: string;
25
25
  readonly status: AccountOrderStatus;
26
26
  readonly paymentStatus: AccountOrderPaymentStatus;
27
- /** Net order total. */
27
+ /** Net goods subtotal (excl. shipping and tax). Live orders only. */
28
+ readonly subtotal?: number;
29
+ /** Shipping cost (net). Live orders only. */
30
+ readonly shipping?: number;
31
+ /** Tax amount included in the total. Live orders only. */
32
+ readonly tax?: number;
33
+ /** Gross order total (incl. shipping and tax) — the amount charged. */
28
34
  readonly total: number;
29
35
  readonly currency: string;
30
36
  /** Customer's own order/PO number from the checkout. */
@@ -67,8 +67,11 @@
67
67
  "date": "Datum",
68
68
  "shippingStatus": "Versandstatus",
69
69
  "paymentStatus": "Zahlungsstatus",
70
- "total": "Summe (netto)",
71
- "yourOrderNumber": "Ihre Bestellnummer"
70
+ "total": "Gesamtbetrag",
71
+ "yourOrderNumber": "Ihre Bestellnummer",
72
+ "subtotal": "Warenwert",
73
+ "shipping": "Versandkosten",
74
+ "tax": "MwSt."
72
75
  },
73
76
  "status": {
74
77
  "processing": "In Bearbeitung",
@@ -113,7 +116,7 @@
113
116
  "id": "Bestellung",
114
117
  "date": "Datum",
115
118
  "positions": "Positionen",
116
- "total": "Summe (netto)",
119
+ "total": "Gesamtbetrag",
117
120
  "status": "Status",
118
121
  "payment": "Zahlung"
119
122
  }
@@ -67,8 +67,11 @@
67
67
  "date": "Date",
68
68
  "shippingStatus": "Shipping status",
69
69
  "paymentStatus": "Payment status",
70
- "total": "Total (net)",
71
- "yourOrderNumber": "Your order number"
70
+ "total": "Total",
71
+ "yourOrderNumber": "Your order number",
72
+ "subtotal": "Subtotal",
73
+ "shipping": "Shipping",
74
+ "tax": "VAT"
72
75
  },
73
76
  "status": {
74
77
  "processing": "Processing",
@@ -113,7 +116,7 @@
113
116
  "id": "Order",
114
117
  "date": "Date",
115
118
  "positions": "Items",
116
- "total": "Total (net)",
119
+ "total": "Total",
117
120
  "status": "Status",
118
121
  "payment": "Payment"
119
122
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/cover",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
4
4
  "description": "Cover \u2014 revenexx design system for Nuxt. Distributed as a Nuxt layer: generic UI components, theming tokens and stores shared by the demo shop, custom storefronts and the Blokkli theme.",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
@@ -163,7 +163,10 @@ export default defineEventHandler(async (event) => {
163
163
  // Live shipping: the chosen method must be a real rate for this
164
164
  // buyer context; its price replaces the demo calculation's shipping
165
165
  // line in the order totals and the amount the payment is created over.
166
- let liveShippingAdjustment = 0;
166
+ // The chosen live shipping rate carries its own price AND tax_rate
167
+ // (resolved by the shipping app from markets.tax_classes). The order app
168
+ // taxes shipping with it — the BFF computes no tax itself.
169
+ let liveShippingRate: { price: number; tax_rate: number } | null = null;
167
170
  if (liveShipping && body.deliveryMethod) {
168
171
  const address = body.address as Record<string, unknown> | undefined;
169
172
  try {
@@ -171,13 +174,12 @@ export default defineEventHandler(async (event) => {
171
174
  orderValue: calculation.totals.find(row => row.key === "subtotal")?.amount
172
175
  ?? calculation.totals.find(row => row.key === "total")?.amount ?? 0,
173
176
  country: String(address?.country ?? ""),
174
- }) as unknown as { rates: Array<{ code: string; price: number }> };
177
+ }) as unknown as { rates: Array<{ code: string; price: number; tax_rate: number | null }> };
175
178
  const rate = rates.find(r => r.code === body.deliveryMethod);
176
179
  if (!rate) {
177
180
  throw createError({ status: 422, message: `Delivery method '${body.deliveryMethod}' is not available for this order` });
178
181
  }
179
- const mockShipping = calculation.totals.find(row => row.key === "shipping")?.amount ?? 0;
180
- liveShippingAdjustment = rate.price - mockShipping;
182
+ liveShippingRate = { price: rate.price, tax_rate: Number(rate.tax_rate ?? 0) };
181
183
  }
182
184
  catch (err) {
183
185
  if (isError(err)) {
@@ -196,6 +198,9 @@ export default defineEventHandler(async (event) => {
196
198
  // approvals domain is deliberately not part of OM phase 1.
197
199
  const liveOrders = resolveOrderServiceKey(event) === "api" && blockingApprovals.length === 0;
198
200
  let liveOrderUuid: string | null = null;
201
+ // The placed order's grand_total (orders app = source of truth) drives
202
+ // the payment amount, so payment and order can never diverge.
203
+ let liveOrderTotal: number | null = null;
199
204
  if (liveOrders) {
200
205
  const address = body.address as Record<string, unknown>;
201
206
  const billing = address?.billingAddressSameAsShipping === false
@@ -213,6 +218,28 @@ export default defineEventHandler(async (event) => {
213
218
  // order history, filtered strictly by contact_id).
214
219
  const contactId = refs.contact_id ?? user?.contactId;
215
220
  const organizationId = refs.organization_id ?? user?.organizationId;
221
+
222
+ // Per-item tax rate from the prices app (same resolve that prices
223
+ // the storefront), not a BFF constant — one bulk call. The orders
224
+ // app then taxes items + shipping and computes grand_total itself.
225
+ const orderItems = body.items as Array<Record<string, unknown>>;
226
+ const itemRate = new Map<string, number>();
227
+ try {
228
+ const { prices: resolved } = await useRevenexxSdk().prices.pricesResolve({
229
+ items: orderItems.map(it => ({ product_id: String(it.id), sku: it.sku ? String(it.sku) : undefined, quantity: Number(it.quantity) || 1 })) as Models.PriceResolveItem[],
230
+ ...(contactId ? { contactId } : {}),
231
+ ...(organizationId ? { organizationId } : {}),
232
+ }) as unknown as { prices: Array<{ product_id: string | null; sku: string | null; tax_rate: number | null }> };
233
+ for (const p of resolved) {
234
+ if (p.tax_rate == null) continue;
235
+ if (p.product_id) itemRate.set(p.product_id, p.tax_rate);
236
+ if (p.sku) itemRate.set(p.sku, p.tax_rate);
237
+ }
238
+ }
239
+ catch (rateErr) {
240
+ getLogService().error("Item tax-rate resolution failed", apiErrorContext(rateErr));
241
+ }
242
+
216
243
  const placed = await useRevenexxSdk().orders.ordersPlace({
217
244
  ...(contactId ? { contactId } : {}),
218
245
  ...(organizationId ? { organizationId } : {}),
@@ -222,20 +249,21 @@ export default defineEventHandler(async (event) => {
222
249
  billingAddress: (billing ?? null) as object,
223
250
  shippingAddress: (address ?? null) as object,
224
251
  payment: { method: payment.method },
252
+ // Shipping price + tax_rate from the shipping app; the orders
253
+ // app taxes it and computes grand_total — the BFF sends NO
254
+ // grand_total (orders is the single source of truth).
225
255
  shipping: {
226
256
  method: body.deliveryMethod ?? "standard",
227
- price: Math.max(0, (shippingRow?.amount ?? 0) + liveShippingAdjustment),
257
+ price: Math.max(0, liveShippingRate?.price ?? shippingRow?.amount ?? 0),
258
+ tax_rate: liveShippingRate?.tax_rate ?? 0,
228
259
  },
229
- // The order carries what the customer saw (and the payment
230
- // charges): the checkout calculation's total.
231
- grandTotal: Math.max(0, Math.round(((totalRow?.amount ?? 0) + liveShippingAdjustment) * 100) / 100),
232
- items: (body.items as Array<Record<string, unknown>>).map((item, index) => ({
260
+ items: orderItems.map((item, index) => ({
233
261
  product_id: String(item.id),
234
262
  sku: String(item.sku ?? "") || undefined,
235
263
  name: String(item.name ?? item.sku ?? item.id),
236
264
  quantity: calculation.lines[index]?.adjustedQuantity ?? Number(item.quantity),
237
265
  unit_price: calculation.lines[index]?.unitPrice ?? Number(item.price ?? 0),
238
- tax_rate: 19,
266
+ tax_rate: itemRate.get(String(item.id)) ?? itemRate.get(String(item.sku ?? "")) ?? 0,
239
267
  product: {
240
268
  ...(item.image ? { image: String(item.image) } : {}),
241
269
  ...(item.categorySlug ? { categorySlug: String(item.categorySlug) } : {}),
@@ -248,9 +276,10 @@ export default defineEventHandler(async (event) => {
248
276
  ...(body.partialDelivery !== undefined ? { partial_delivery: body.partialDelivery } : {}),
249
277
  ...(body.requestedDate ? { requested_date: body.requestedDate } : {}),
250
278
  },
251
- }) as unknown as { id: string; number: string };
279
+ }) as unknown as { id: string; number: string; grand_total?: number };
252
280
  orderId = placed.number;
253
281
  liveOrderUuid = placed.id;
282
+ liveOrderTotal = typeof placed.grand_total === "number" ? placed.grand_total : null;
254
283
  }
255
284
  catch (err) {
256
285
  if (err instanceof RevenexxException && (err.code === 400 || err.code === 422)) {
@@ -276,7 +305,8 @@ export default defineEventHandler(async (event) => {
276
305
  try {
277
306
  const created = await useRevenexxSdk().payments.paymentsCreate({
278
307
  methodCode: payment.method,
279
- amount: Math.max(0, Math.round(((totalRow?.amount ?? 0) + liveShippingAdjustment) * 100) / 100),
308
+ // Charge exactly what the orders app computed (incl. shipping tax).
309
+ amount: Math.max(0, Math.round((liveOrderTotal ?? totalRow?.amount ?? 0) * 100) / 100),
280
310
  currency: calculation.currency,
281
311
  country,
282
312
  orderRef: orderId,
@@ -29,6 +29,9 @@ export interface LiveOrder {
29
29
  payment_status: string;
30
30
  fulfillment_status: string;
31
31
  currency: string;
32
+ subtotal: number;
33
+ shipping_total: number;
34
+ tax_total: number;
32
35
  grand_total: number;
33
36
  billing_address: Record<string, unknown> | null;
34
37
  shipping_address: Record<string, unknown> | null;
@@ -113,6 +116,9 @@ export function mapLiveOrderToAccount(order: LiveOrder): AccountOrder {
113
116
  date: day(placed),
114
117
  status: mapStatus(order),
115
118
  paymentStatus: mapPaymentStatus(order.payment_status),
119
+ subtotal: Number(order.subtotal),
120
+ shipping: Number(order.shipping_total),
121
+ tax: Number(order.tax_total),
116
122
  total: Number(order.grand_total),
117
123
  currency: order.currency,
118
124
  ...(order.customer_order_number ? { orderNumber: order.customer_order_number } : {}),