@revenexx/cover 0.1.6 → 0.1.8

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.
@@ -1,10 +1,12 @@
1
1
  <script setup lang="ts">
2
- import { legalLinks, shopContact } from "../config/navigation";
2
+ import { legalLinks as defaultLegalLinks, shopContact } from "../config/navigation";
3
3
 
4
4
  // Distraction-free layout for conversion-critical pages — authentication
5
5
  // (login, register, recovery) and the checkout funnel. No shop chrome:
6
6
  // logo + back link on top, the page content, and a slim footer with the
7
7
  // essential links (contact + legal).
8
+ const appConfig = useAppConfig();
9
+ const legalLinks = computed(() => (appConfig.legalLinks as typeof defaultLegalLinks | undefined) ?? defaultLegalLinks);
8
10
  const { t } = useI18n();
9
11
  const localePath = useLocalePath();
10
12
  const head = useLocaleHead({ seo: true });
@@ -65,7 +67,7 @@ const tinted = computed(() => /\/checkout(\/|$)/.test(route.path));
65
67
  <NuxtLink
66
68
  v-for="link in legalLinks"
67
69
  :key="link.to"
68
- :to="link.to"
70
+ :to="localePath(link.to)"
69
71
  tabindex="0"
70
72
  class="text-xs text-dimmed hover:text-default transition-colors"
71
73
  >
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revenexx/cover",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
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",
@@ -7,7 +7,7 @@ export default defineEventHandler(async (event) => {
7
7
  const user = await getAuthService(event).me(event);
8
8
 
9
9
  try {
10
- return await getB2BContextService(event).getContext(user?.$id ?? null);
10
+ return await getB2BContextService(event).getContext(user?.$id ?? null, user?.role ?? null);
11
11
  }
12
12
  catch (err) {
13
13
  getLogService().error("Service error: account/context", toErrorContext(err));
@@ -15,7 +15,7 @@ export default defineEventHandler(async (event) => {
15
15
  const user = await getAuthService(event).me(event);
16
16
 
17
17
  try {
18
- const context = await getB2BContextService(event).getContext(user?.$id ?? null);
18
+ const context = await getB2BContextService(event).getContext(user?.$id ?? null, user?.role ?? null);
19
19
  return await getCartCalculationService().calculate(body, context, locale);
20
20
  }
21
21
  catch (err) {
@@ -14,7 +14,7 @@ export default defineEventHandler(async (event) => {
14
14
 
15
15
  const locale = resolveLocale(event);
16
16
  const user = await getAuthService(event).me(event);
17
- const context = await getB2BContextService(event).getContext(user?.$id ?? null);
17
+ const context = await getB2BContextService(event).getContext(user?.$id ?? null, user?.role ?? null);
18
18
 
19
19
  if (!context.settings.cart.exportEnabled) {
20
20
  throw createError({ statusCode: 403, message: "Cart export is disabled" });
@@ -38,6 +38,30 @@ function isNonEmptyString(v: unknown): v is string {
38
38
  return typeof v === "string" && v.trim().length > 0;
39
39
  }
40
40
 
41
+ // Customer addresses may carry the country as a display name ("Deutschland",
42
+ // "Austria") — the payments app's eligibility check expects ISO 3166-1
43
+ // alpha-2 codes. Two-letter values pass through unchanged.
44
+ const COUNTRY_NAME_TO_ISO: Record<string, string> = {
45
+ deutschland: "DE", germany: "DE",
46
+ österreich: "AT", austria: "AT",
47
+ schweiz: "CH", switzerland: "CH",
48
+ niederlande: "NL", netherlands: "NL",
49
+ frankreich: "FR", france: "FR",
50
+ italien: "IT", italy: "IT",
51
+ spanien: "ES", spain: "ES",
52
+ belgien: "BE", belgium: "BE",
53
+ polen: "PL", poland: "PL",
54
+ tschechien: "CZ", "czech republic": "CZ", czechia: "CZ",
55
+ };
56
+
57
+ function toIsoCountry(value: unknown): string {
58
+ const raw = String(value ?? "").trim();
59
+ if (/^[A-Za-z]{2}$/.test(raw)) {
60
+ return raw.toUpperCase();
61
+ }
62
+ return COUNTRY_NAME_TO_ISO[raw.toLowerCase()] ?? raw;
63
+ }
64
+
41
65
  function validateAddress(address: Record<string, unknown>, customerType: "business" | "private"): string | null {
42
66
  const requiredFields = customerType === "private"
43
67
  ? REQUIRED_ADDRESS_FIELDS_PRIVATE
@@ -122,7 +146,7 @@ export default defineEventHandler(async (event) => {
122
146
  // Approval limits are checked at order time: exceeding a blocking
123
147
  // limit turns the order into an approval request (it does not fail).
124
148
  const user = await getAuthService(event).me(event);
125
- const context = await getB2BContextService(event).getContext(user?.$id ?? null);
149
+ const context = await getB2BContextService(event).getContext(user?.$id ?? null, user?.role ?? null);
126
150
  const locale = resolveLocale(event);
127
151
  const calculation = await getCartCalculationService().calculate(
128
152
  {
@@ -240,7 +264,7 @@ export default defineEventHandler(async (event) => {
240
264
  const billing = address?.billingAddressSameAsShipping === false
241
265
  ? address?.billing as Record<string, unknown> | undefined
242
266
  : address;
243
- const country = String(body.billingCountry ?? billing?.country ?? "");
267
+ const country = toIsoCountry(body.billingCountry ?? billing?.country ?? "");
244
268
  try {
245
269
  const created = await useRevenexxSdk().payments.paymentsCreate({
246
270
  methodCode: payment.method,
@@ -273,6 +297,7 @@ export default defineEventHandler(async (event) => {
273
297
  throw err;
274
298
  }
275
299
  if (err instanceof RevenexxException && (err.code === 400 || err.code === 422)) {
300
+ getLogService().error("Payment rejected", apiErrorContext(err));
276
301
  throw createError({ status: 422, message: err.message });
277
302
  }
278
303
  getLogService().error("Payment creation failed", apiErrorContext(err));
@@ -25,7 +25,7 @@ export default defineEventHandler(async (event) => {
25
25
  throw createError({ statusCode: 400, message: "workflowId and items[] required" });
26
26
  }
27
27
 
28
- const context = await getB2BContextService(event).getContext(user.$id);
28
+ const context = await getB2BContextService(event).getContext(user.$id, user.role ?? null);
29
29
  if (!context.settings.workflows.enabled) {
30
30
  throw createError({ statusCode: 403, message: "Workflows are disabled" });
31
31
  }
@@ -7,8 +7,8 @@
7
7
  "$formkit": "text",
8
8
  "name": "postalCode",
9
9
  "label": "fields.postalCode",
10
- "validation": "required|matches:/^[0-9]{4,10}$/",
11
- "validationMessages": { "matches": "validation.postalCodeFormat" },
10
+ "validation": "required|matches:/^[0-9]+$/|length:4,10",
11
+ "validationMessages": { "matches": "validation.postalCodeFormat", "length": "validation.postalCodeFormat" },
12
12
  "autocomplete": "postal-code",
13
13
  "inputmode": "numeric"
14
14
  },
@@ -1,4 +1,4 @@
1
- import type { B2BContext, Requisition } from "../../app/interfaces/b2b";
1
+ import type { B2BContext, B2BRole, Requisition } from "../../app/interfaces/b2b";
2
2
 
3
3
  /**
4
4
  * Service contract for the per-user B2B context: organization settings,
@@ -7,8 +7,12 @@ import type { B2BContext, Requisition } from "../../app/interfaces/b2b";
7
7
  * Register the active implementation via app.config → b2bService.
8
8
  */
9
9
  export interface IB2BContextService {
10
- /** Context for a user; `null` userId returns the guest context. */
11
- getContext(userId: string | null): Promise<B2BContext>;
10
+ /**
11
+ * Context for a user; `null` userId returns the guest context.
12
+ * `fallbackRole` is the authenticated session's role — it applies when
13
+ * the user has no persona record (live logins against the public API).
14
+ */
15
+ getContext(userId: string | null, fallbackRole?: B2BRole | null): Promise<B2BContext>;
12
16
  }
13
17
 
14
18
  /**
@@ -1,4 +1,4 @@
1
- import type { B2BContext, ApprovalLimit, ApprovalWorkflow, CostCenter, OrganizationSettings, Requisition } from "../../app/interfaces/b2b";
1
+ import type { B2BContext, B2BRole, ApprovalLimit, ApprovalWorkflow, CostCenter, OrganizationSettings, Requisition } from "../../app/interfaces/b2b";
2
2
  import type { AuthRole } from "../../app/interfaces/auth";
3
3
 
4
4
  import type { IB2BContextService, IRequisitionService } from "../interfaces/b2bContext";
@@ -30,7 +30,7 @@ const GUEST_ROLE = null;
30
30
  export class MockB2BContextService implements IB2BContextService {
31
31
  constructor(private readonly settingsOverride?: Partial<OrganizationSettings>) {}
32
32
 
33
- async getContext(userId: string | null): Promise<B2BContext> {
33
+ async getContext(userId: string | null, fallbackRole?: B2BRole | null): Promise<B2BContext> {
34
34
  const org = await readCoverConfigJson<OrganizationConfig>("account/organization.json");
35
35
  const settings = this.mergeSettings(org.settings);
36
36
 
@@ -47,6 +47,8 @@ export class MockB2BContextService implements IB2BContextService {
47
47
 
48
48
  const personas = await readCoverConfigJson<PersonaConfig[]>("account/personas.json");
49
49
  const persona = personas.find(p => p.id === userId);
50
+ // Live logins have no persona record — the session's role applies.
51
+ const role = persona?.role ?? fallbackRole ?? GUEST_ROLE;
50
52
  const overrides = await readGovernanceOverrides();
51
53
 
52
54
  const baseWorkflows = await readCoverConfigJson<ApprovalWorkflow[]>("account/workflows.json");
@@ -67,7 +69,7 @@ export class MockB2BContextService implements IB2BContextService {
67
69
  // Approval limits only apply when actually ordering — requesters never
68
70
  // order directly, so their context carries none (limits are not
69
71
  // checked for requisitions).
70
- const limitsApply = settings.approvals.enabled && persona?.role !== "requester";
72
+ const limitsApply = settings.approvals.enabled && role !== "requester";
71
73
 
72
74
  const costCenterLimits = limitsApply
73
75
  ? costCenters.flatMap(cc => (cc.limit ? [cc.limit] : []))
@@ -83,7 +85,7 @@ export class MockB2BContextService implements IB2BContextService {
83
85
  : [];
84
86
 
85
87
  return {
86
- role: persona?.role ?? GUEST_ROLE,
88
+ role,
87
89
  settings,
88
90
  costCenters,
89
91
  defaultCostCenterId: settings.costCenters.enabled
@@ -84,7 +84,7 @@ export function apiAuthErrorType(err: RevenexxException): string {
84
84
  /** Diagnostic fields for structured logs — never includes the API key. */
85
85
  export function apiErrorContext(err: unknown): Record<string, unknown> {
86
86
  if (err instanceof RevenexxException) {
87
- return { apiStatus: err.code, apiType: err.type, apiMessage: err.message };
87
+ return { apiStatus: err.code, apiType: err.type, apiMessage: err.message, apiResponse: String(err.response ?? "").slice(0, 600) };
88
88
  }
89
89
  return { errorMessage: err instanceof Error ? err.message : String(err) };
90
90
  }