PageSourceSearch

https://invoicecraftly.com/_assets/js/features/invoice-compliance/countries/norway-peppol.810efa18c7ca.js

js invoicecraftly.com collected 2026-10-02 01:35:52 UTC 7,953 bytes, 198 lines download raw bytes

1import {
2  COMPLIANCE_SEVERITY,
3  COMPLIANCE_STATUS,
4  createComplianceDiagnostic
5} from '../contracts.84ed5d8e89eb.js';
6import { normalizeComplianceSupplement } from '../semantic/supplement.0004a4d51d32.js';
7
8const NORWAY_PEPPOL_EVIDENCE = Object.freeze([
9  Object.freeze({
10    authority: 'OpenPeppol',
11    title: 'Peppol BIS Billing 3.0 — Norway rule NO-R-001',
12    url: 'https://docs.peppol.eu/poacc/billing/3.0/upcoming/rules/ubl-peppol/NO-R-001/',
13    reviewedAt: '2026-08-16'
14  }),
15  Object.freeze({
16    authority: 'OpenPeppol',
17    title: 'Peppol BIS Billing 3.0 — Norway rule NO-R-002',
18    url: 'https://docs.peppol.eu/poacc/billing/3.0/upcoming/rules/ubl-peppol/NO-R-002/',
19    reviewedAt: '2026-08-16'
20  }),
21  Object.freeze({
22    authority: 'Altinn',
23    title: 'Value added tax — no separate Norwegian VAT number',
24    url: 'https://info.altinn.no/en/start-and-run-business/direct-and-indirect-taxes/indirect-taxes/value-added-tax',
25    reviewedAt: '2026-08-16'
26  })
27]);
28
29function digits(value) {
30  return String(value || '').replace(/\D/g, '');
31}
32
33export function isValidNorwegianOrganizationNumber(value) {
34  const normalized = digits(value);
35  if (!/^\d{9}$/.test(normalized)) return false;
36  const weights = [3, 2, 7, 6, 5, 4, 3, 2];
37  const sum = weights.reduce((total, weight, index) => total + (Number(normalized[index]) * weight), 0);
38  const remainder = sum % 11;
39  const check = remainder === 0 ? 0 : 11 - remainder;
40  return check !== 10 && Number(normalized[8]) === check;
41}
42
43export function buildNorwayVatIdentifier(organizationNumber) {
44  const normalized = digits(organizationNumber);
45  if (!isValidNorwegianOrganizationNumber(normalized)) return '';
46  return `NO${normalized}MVA`;
47}
48
49function copyParty(party) {
50  return {
51    postalAddress: { ...(party?.postalAddress || {}) },
52    electronicAddressSchemeId: party?.electronicAddressSchemeId || '',
53    taxId: { ...(party?.taxId || {}) },
54    vatIdentifier: party?.vatIdentifier || '',
55    taxRegistrationIdentifier: { ...(party?.taxRegistrationIdentifier || {}) }
56  };
57}
58
59export function buildNorwayPeppolSupplement(snapshot, supplementInput = {}) {
60  const base = normalizeComplianceSupplement(supplementInput);
61  const seller = copyParty(base.seller);
62  const buyer = copyParty(base.buyer);
63  const organizationNumber = digits(snapshot?.seller?.taxId);
64  const validOrganizationNumber = isValidNorwegianOrganizationNumber(organizationNumber);
65
66  if (validOrganizationNumber && !seller.taxId.role) {
67    seller.taxId = {
68      role: 'legal-registration',
69      schemeId: '0192',
70      taxSchemeId: ''
71    };
72  }
73
74  if (
75    validOrganizationNumber
76    && snapshot?.seller?.registration?.vatStatus === 'registered'
77    && !seller.vatIdentifier
78  ) {
79    seller.vatIdentifier = buildNorwayVatIdentifier(organizationNumber);
80  }
81
82  const legalForm = String(snapshot?.seller?.registration?.legalForm || '').toLowerCase();
83  const businessRegisterStatus = String(snapshot?.seller?.registration?.businessRegisterStatus || '').toLowerCase();
84  if (
85    ['as', 'asa', 'nuf'].includes(legalForm)
86    && businessRegisterStatus === 'registered'
87    && !seller.taxRegistrationIdentifier.value
88  ) {
89    seller.taxRegistrationIdentifier = {
90      value: 'Foretaksregisteret',
91      taxSchemeId: 'TAX'
92    };
93  }
94
95  return normalizeComplianceSupplement({
96    version: 1,
97    buyerReference: base.buyerReference,
98    seller,
99    buyer,
100    lines: base.lines.map((line) => ({ ...line }))
101  });
102}
103
104function organizationNumberDiagnostic(value, path, id) {
105  if (!value) return null;
106  return createComplianceDiagnostic({
107    id,
108    status: isValidNorwegianOrganizationNumber(value) ? COMPLIANCE_STATUS.PASS : COMPLIANCE_STATUS.FAIL,
109    severity: isValidNorwegianOrganizationNumber(value) ? COMPLIANCE_SEVERITY.INFO : COMPLIANCE_SEVERITY.ERROR,
110    message: isValidNorwegianOrganizationNumber(value)
111      ? 'Norwegian organisation number passes the modulus-11 structural check.'
112      : 'Norwegian organisation number must be nine digits with a valid modulus-11 check digit.',
113    path,
114    capability: 'norway-peppol-identifier-format'
115  });
116}
117
118export function evaluateNorwayPeppolOverlay(snapshot, semanticResult) {
119  const diagnostics = [];
120  const semanticInvoice = semanticResult?.invoice || null;
121  if (!semanticInvoice) {
122    diagnostics.push(createComplianceDiagnostic({
123      id: 'NO-PEPPOL-SEMANTIC-RESULT-MISSING',
124      status: COMPLIANCE_STATUS.UNSUPPORTED,
125      severity: COMPLIANCE_SEVERITY.WARNING,
126      message: 'Norway Peppol overlay requires an EN16931 semantic result.',
127      capability: 'norway-peppol-overlay'
128    }));
129    return Object.freeze({ diagnostics: Object.freeze(diagnostics), evidence: NORWAY_PEPPOL_EVIDENCE });
130  }
131
132  const sellerCountry = semanticInvoice.seller?.address?.countryCode || '';
133  if (sellerCountry && sellerCountry !== 'NO') {
134    diagnostics.push(createComplianceDiagnostic({
135      id: 'NO-PEPPOL-SELLER-COUNTRY',
136      status: COMPLIANCE_STATUS.UNSUPPORTED,
137      severity: COMPLIANCE_SEVERITY.WARNING,
138      message: `Norway country overlay does not apply to seller country ${sellerCountry}.`,
139      path: 'seller.address.countryCode',
140      capability: 'norway-peppol-overlay'
141    }));
142    return Object.freeze({ diagnostics: Object.freeze(diagnostics), evidence: NORWAY_PEPPOL_EVIDENCE });
143  }
144
145  const vatIdentifier = semanticInvoice.seller?.identifiers?.vatIdentifier?.value || '';
146  if (vatIdentifier) {
147    const match = /^NO(\d{9})MVA$/.exec(vatIdentifier);
148    const valid = Boolean(match && isValidNorwegianOrganizationNumber(match[1]));
149    diagnostics.push(createComplianceDiagnostic({
150      id: 'NO-R-001',
151      status: valid ? COMPLIANCE_STATUS.PASS : COMPLIANCE_STATUS.FAIL,
152      severity: valid ? COMPLIANCE_SEVERITY.INFO : COMPLIANCE_SEVERITY.ERROR,
153      message: valid
154        ? 'Norwegian seller VAT identifier matches NO + valid nine-digit organisation number + MVA.'
155        : 'Norwegian seller VAT identifier must be NO followed by a valid nine-digit organisation number and MVA.',
156      path: 'seller.vatIdentifier',
157      capability: 'norway-peppol-country-rule'
158    }));
159  }
160
161  const legalId = semanticInvoice.seller?.identifiers?.legalRegistrationIdentifier;
162  if (legalId?.schemeId === '0192') {
163    const diagnostic = organizationNumberDiagnostic(legalId.value, 'seller.taxId', 'PEPPOL-COMMON-R041-SELLER-LEGAL-ID');
164    if (diagnostic) diagnostics.push(diagnostic);
165  }
166  for (const [role, endpoint] of [
167    ['seller', semanticInvoice.seller?.electronicAddress],
168    ['buyer', semanticInvoice.buyer?.electronicAddress]
169  ]) {
170    if (endpoint?.schemeId === '0192') {
171      const diagnostic = organizationNumberDiagnostic(endpoint.value, `${role}.electronicAddress`, `PEPPOL-COMMON-R041-${role.toUpperCase()}-ENDPOINT`);
172      if (diagnostic) diagnostics.push(diagnostic);
173    }
174  }
175
176  const legalForm = String(snapshot?.seller?.registration?.legalForm || '').toLowerCase();
177  if (['as', 'asa', 'nuf'].includes(legalForm)) {
178    const taxRegistration = semanticInvoice.seller?.identifiers?.taxRegistrationIdentifier;
179    const hasRegisterText = taxRegistration?.value === 'Foretaksregisteret' && taxRegistration?.taxSchemeId === 'TAX';
180    diagnostics.push(createComplianceDiagnostic({
181      id: 'NO-R-002',
182      status: hasRegisterText ? COMPLIANCE_STATUS.PASS : COMPLIANCE_STATUS.REVIEW_REQUIRED,
183      severity: hasRegisterText ? COMPLIANCE_SEVERITY.INFO : COMPLIANCE_SEVERITY.WARNING,
184      message: hasRegisterText
185        ? 'Norwegian company-register presentation is represented as Foretaksregisteret with TAX scheme.'
186        : 'Review the Norwegian Foretaksregisteret presentation requirement for this legal form.',
187      path: 'seller.registration.businessRegisterStatus',
188      capability: 'norway-peppol-country-rule'
189    }));
190  }
191
192  return Object.freeze({
193    diagnostics: Object.freeze(diagnostics),
194    evidence: NORWAY_PEPPOL_EVIDENCE
195  });
196}
197
198export { NORWAY_PEPPOL_EVIDENCE };

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.