Reviewers (PMD-3131):

User Story

As a Communication Service Provider, I want the system to generate invoices in EN 16931-compliant structured electronic formats (such as XML), so that I can meet EU e-invoicing requirements without manual intervention and allow business customers to import invoices directly into their ERP and accounting systems.

Example of use

A CSP issues monthly invoices to its clients. With EN 16931-compliant e-invoicing integrated into PortaSwitch, these invoices are automatically generated in a structured electronic format (e.g., XML) that is machine-readable and meets the European standard. This allows for seamless processing by clients' accounting systems and compliance with EU regulations.

Business model

Postpaid B2B, B2C

Technology

  • Electronic Invoicing Standard: EN 16931
  • Invoice Formats: Universal Business Language (UBL) and UN/CEFACT Cross Industry Invoice (CII)

Current Solution

PortaSwitch currently generates invoices in the PDF format. However, it lacks native support for exporting invoices in XML formats compliant with EN 16931, limiting interoperability with systems that require standardized electronic invoices.

Stakeholders and their benefits

Benefit /
Stakeholders
More
Comfort
Increased
Efficiency
Saves
Time
Tighter
Control
Replaces
Human
Regulatory
Requirement
CSP




(tick) 
Resellers / distributors




(tick) 
3rd party(tick) (tick) (tick) (tick) (tick) 
End user(tick) (tick) (tick) (tick) (tick) (tick) 

Use Cases

Use case #1: Generating EN 16931-Compliant Electronic Invoices

Roles: Admin/Reseller, PortaSwitch, (Sub)customer

Preconditions:

  • A (sub)customer, "ABC Corp." is registered in PortaSwitch.
  • "ABC Corp." has an active service agreement with a monthly billing.
  • Previous invoices have been generated in PDF format.
  • "ABC Corp." has outstanding due amounts from prior billing cycles.

Use scenario #1.1aRegular electronic invoices for EN 16931 Compliance (happy path)

  • The Admin/Reseller enables the option for EN 16931-compliant invoice generation on March 1, 2025, at 10:00 AM
  • On April 1, 2025, PortaSwitch automatically generates the monthly invoice for "ABC Corp."
  • The invoice includes all mandatory EN 16931 elements, such as invoice number, issue date, supplier and customer details, tax information, line items with descriptions, quantities, unit prices, and total amounts
XML example

<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
         xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
         xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
    <!-- Invoice Identification -->
    <cbc:ID>{{ invoice.invoice_number }}</cbc:ID>
    <cbc:IssueDate>{{ invoice.issue_date }}</cbc:IssueDate>
    <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode> <!-- 380 represents a standard invoice -->
    <cbc:DocumentCurrencyCode>{{ invoice.iso_4217 }}</cbc:DocumentCurrencyCode>

    <!-- Supplier Information -->
    <cac:AccountingSupplierParty>
        <cac:Party>
            <cac:PartyName>
                <cbc:Name>{{ env.companyname }}</cbc:Name>
            </cac:PartyName>
            <cac:PostalAddress>
                <cbc:StreetName>{{ env.addr1 }}</cbc:StreetName>
                <cbc:AdditionalStreetName>{{ env.addr2 }}</cbc:AdditionalStreetName>
                <cbc:CityName>{{ env.contact_city }}</cbc:CityName>
                <cbc:PostalZone>{{ env.contact_zip }}</cbc:PostalZone>
                <cbc:Country>
                    <cbc:IdentificationCode>{{ env.iso_3166_1_a2 }}</cbc:IdentificationCode>
                </cbc:Country>
            </cac:PostalAddress>
            <cac:PartyTaxScheme>
                <cbc:CompanyID>{{ env.tax_id }}</cbc:CompanyID>
                <cac:TaxScheme>
                    <cbc:ID>VAT</cbc:ID>
                </cac:TaxScheme>
            </cac:PartyTaxScheme>
            <cac:Contact>
                <cbc:Telephone>{{ env.phone }}</cbc:Telephone>
                <cbc:ElectronicMail>{{ env.email }}</cbc:ElectronicMail>
            </cac:Contact>
        </cac:Party>
    </cac:AccountingSupplierParty>

    <!-- Customer Information -->
    <cac:AccountingCustomerParty>
        <cac:Party>
            <cac:PartyName>
                <cbc:Name>{{ customer.companyname }}</cbc:Name>
            </cac:PartyName>
            <cac:PostalAddress>
                <cbc:StreetName>{{ customer.address }}</cbc:StreetName>
                <cbc:CityName>{{ customer.city }}</cbc:CityName>
                <cbc:PostalZone>{{ customer.zip }}</cbc:PostalZone>
                <cbc:Country>
                    <cbc:IdentificationCode>{{ customer.country }}</cbc:IdentificationCode>
                </cbc:Country>
            </cac:PostalAddress>
            <cac:PartyTaxScheme>
                <cbc:CompanyID>{{ customer.tax_id }}</cbc:CompanyID>
                <cac:TaxScheme>
                    <cbc:ID>VAT</cbc:ID>
                </cac:TaxScheme>
            </cac:PartyTaxScheme>
            <cac:Contact>
                <cbc:Telephone>{{ customer.phone1 }}</cbc:Telephone>
                <cbc:ElectronicMail>{{ customer.email }}</cbc:ElectronicMail>
            </cac:Contact>
        </cac:Party>
    </cac:AccountingCustomerParty>

    <!-- Payment Means -->
    <cac:PaymentMeans>
        <cbc:PaymentMeansCode>42</cbc:PaymentMeansCode> <!-- 42 represents SEPA Credit Transfer -->
        <cac:PayeeFinancialAccount>
            <cbc:ID>{{ env.bank_account }}</cbc:ID> <!-- Assuming 'env.bank_account' holds the IBAN -->
            <cac:FinancialInstitutionBranch>
                <cac:FinancialInstitution>
                    <cbc:ID>{{ env.bank_bic }}</cbc:ID> <!-- Assuming 'env.bank_bic' holds the BIC -->
                </cac:FinancialInstitution>
            </cac:FinancialInstitutionBranch>
        </cac:PayeeFinancialAccount>
    </cac:PaymentMeans>

    <!-- Tax Total -->
    <cac:TaxTotal>
        <cbc:TaxAmount currencyID="{{ invoice.iso_4217 }}">{{ invoice.taxes }}</cbc:TaxAmount>
        <!-- Tax Subtotal can be added here if multiple tax categories are present -->
    </cac:TaxTotal>

    <!-- Legal Monetary Total -->
    <cac:LegalMonetaryTotal>
        <cbc:LineExtensionAmount currencyID="{{ invoice.iso_4217 }}">{{ invoice.amount_net }}</cbc:LineExtensionAmount>
        <cbc:TaxExclusiveAmount currencyID="{{ invoice.iso_4217 }}">{{ invoice.subtotal_without_taxes }}</cbc:TaxExclusiveAmount>
        <cbc:TaxInclusiveAmount currencyID="{{ invoice.iso_4217 }}">{{ invoice.subtotal }}</cbc:TaxInclusiveAmount>
        <cbc:PayableAmount currencyID="{{ invoice.iso_4217 }}">{{ invoice.amount_due }}</cbc:PayableAmount>
    </cac:LegalMonetaryTotal>

    <!-- Invoice Line Items -->
    <cac:InvoiceLine>
        <cbc:ID>1</cbc:ID>
        <cbc:InvoicedQuantity unitCode="EA">{{ line.quantity }}</cbc:InvoicedQuantity> <!-- 'line.quantity' should represent the quantity -->
        <cbc:LineExtensionAmount currencyID="{{ invoice.iso_4217 }}">{{ line.total_amount }}</cbc:LineExtensionAmount>
        <cac:Item>
            <cbc:Name>{{ line.description }}</cbc:Name>
        </cac:Item>
        <cac:Price>
            <cbc:PriceAmount currencyID="{{ invoice.iso_4217 }}">{{ line.unit_price }}</cbc:PriceAmount>
        </cac:Price>
    </cac:InvoiceLine>
    <!-- Repeat <cac:InvoiceLine> for each line item -->
</Invoice>


  • PortaSwitch sends the electronic invoice to "ABC Corp." via email (both PDF and XML, or XML only)
  • (Sub)customer imports the XML to their ERP for processing

  • (Sub)customer arranges a payment according to the specified terms
  • Some time later, (Sub)customer needs to access the XML again, so they download it (e.g. via the self-care portal)

Use scenario #1.1b: Regular electronic invoices for EN 16931 Compliance (failed XML, alternative to #1.1b)

  • On April 1, 2025,  PortaSwitch automatically generates the monthly invoice for "ABC Corp."
  • XML generation fails (e.g. due to connectivity issue, invalid data structure etc.)
  • Admin receives a notification about the failure with its reason
  • Admin fixes the data for the invoice (and/or connectivity) and confirms it's ready for XML generation
  • PortaSwitch sends the electronic invoice to "ABC Corp." via email (both PDF and XML, or XML only)

Use scenario #1.2: On-demand Out-of-turn electronic invoices for EN 16931 Compliance

  • The Admin/Reseller identifies the need to issue an out-of-turn invoice for "ABC Corp." on March 15, 2025, at 2:00 PM, due to additional services rendered.
  • The Admin/Reseller manually generates the EN 16931-compliant electronic invoice
  • The system sends the invoice to "ABC Corp." via email (both PDF and XML, or XML only)
  • "ABC Corp." imports the XML into their ERP system for processing
  • "ABC Corp." arranges payment according to the specified terms

Use scenario #1.3: Voiding electronic invoices

  • On April 5, 2025, at 11:00 AM, the Admin/Reseller identifies an error in the invoice sent to "ABC Corp."
  • The Admin/Reseller selects the incorrect invoice and chooses to void it
  • PortaSwitch marks the invoice as voided, visible in the PDF file
  • A new, corrected EN 16931-compliant electronic invoice is automatically generated and sent to "ABC Corp." via email (both PDF and XML, or XML only)
  • The (sub)customer imports the corrected XML into their ERP for processing
  • The (sub)customer adjusts their accounting records correspondingly

Use scenario #1.4: Invoices requiring review

  • On April 1, 2025, PortaSwitch automatically generates the monthly invoice for "ABC Corp." which requires review by Admin (no XML is generated before review)
  • During review, Admin finds an error, corrects data in PortaBilling (re-rating, manual balance adjustments, etc) and re-issues the invoice (no XML is generated before review)
  • Admin reviews the issued invoice and approves it
  • XML is generated after the approval
  • PortaSwitch sends the electronic invoice to "ABC Corp." via email (both PDF and XML, or XML only)
  • (Sub)customer imports the XML to their ERP for processing

  • (Sub)customer arranges a payment according to the specified terms
  • Some time later, (Sub)customer needs to access the XML again, so they download it (e.g. via the self-care portal)

Use scenario #1.5: PDF regeneration

  • Admin decides to regenerate a PDF (e.g. after changing the invoice template)
  • PortaSwitch does not retrigger the new XML generation as the invoice data remains unchanged

Use scenario #1.6: Re-issuing invoices under review

  • On April 1, 2025, PortaSwitch automatically generates the monthly invoice for "ABC Corp." which requires review by Admin
  • Admin decides to re-issue the invoice under review and then approves it
  • XML is generated after the approval
  • PortaSwitch sends the electronic invoice to "ABC Corp." via email (both PDF and XML, or XML only)
  • (Sub)customer imports the XML to their ERP for processing

  • (Sub)customer arranges a payment according to the specified terms
  • Some time later, (Sub)customer needs to access the XML again, so they download it (e.g. via the self-care portal)

Use scenario #1.6: Adjusting invoices

  • Admin cannot adjust the invoice for which an XML was already generated - a new invoice/XML must be generated instead

Non-functional requirements

Peculiarities

  • Example of the Storecove JSON payload for an invoice generated in EU/EEA for a German customer (single line item):
Sample 1
{
  "legalEntityId": LEGAL_ENTITY_ID,
  "routing": {
    "emails": [
      "test@example.com"
    ],
    "eIdentifiers": [
      {
        "scheme": "DE:LWID",
        "id": "10101010-STO-10"
      }
    ]
  },
  "document": {
    "documentType": "invoice",
    "invoice": {
      "invoiceNumber": "202112007",
      "issueDate": "2021-12-07",
      "documentCurrencyCode": "EUR",
      "taxSystem": "tax_line_percentages",
      "accountingCustomerParty": {
        "party": {
          "companyName": "ManyMarkets Inc.",
          "address": {
            "street1": "Street 123",
            "zip": "1111AA",
            "city": "Here",
            "country": "DE"
          }
        },
        "publicIdentifiers": [
          {
            "scheme": "DE:LWID",
            "id": "10101010-STO-10"
          }
        ]
      },
      "invoiceLines": [
        {
          "description": "The things you purchased",
          "amountExcludingVat": 10,
          "tax": {
            "percentage": 19,
            "category": "standard",
            "country": "DE"
          }
        }
      ],
      "taxSubtotals": [
        {
          "percentage": 19,
          "category": "standard",
          "country": "DE",
          "taxableAmount": 10,
          "taxAmount": 1.9
        }
      ],
      "paymentMeansArray": [
        {
          "account": "NL50ABNA0552321249",
          "holder": "Storecove",
          "code": "credit_transfer"
        }
      ],
      "amountIncludingVat": 11.9
    }
  }
}
  • Example of the Storecove JSON payload for an invoice generated in EU/EEA for a Dutch customer (multiple line items):
Sample 2
{
  "legalEntityId": 100000099999,
  "idempotencyGuid": "61b37456-5f9e-4d56-b63b-3b1a23fa5c73",
  "routing": {
    "eIdentifiers": [
      {
        "scheme": "NL:KVK",
        "id": "27375186"
      }
    ],
    "emails": [
      "receiver@example.com"
    ]
  },
  "document": {
    "documentType": "invoice",
    "invoice": {
      "taxSystem": "tax_line_percentages",
      "documentCurrency": "EUR",
      "invoiceNumber": "F463333333336",
      "issueDate": "2020-11-26",
      "taxPointDate": "2020-11-26",
      "dueDate": "2020-12-26",
      "invoicePeriod": "2020-11-12 - 2020-11-17",
      "references": [
        {
          "documentType": "purchase_order",
          "documentId": "buyer reference or purchase order reference is recommended",
          "lineId": "1",
          "issueDate": "2021-12-01"
        },
        {
          "documentType": "buyer_reference",
          "documentId": "buyer reference or purchase order reference is recommended"
        },
        {
          "documentType": "sales_order",
          "documentId": "R06788111"
        },
        {
          "documentType": "billing",
          "documentId": "refers to a previous invoice, used for credit notes"
        },
        {
          "documentType": "contract",
          "documentId": "contract123"
        },
        {
          "documentType": "despatch_advice",
          "documentId": "DDT123"
        },
        {
          "documentType": "receipt",
          "documentId": "aaaaxxxx"
        },
        {
          "documentType": "originator",
          "documentId": "bbbbyyyy"
        },
        {
          "documentType": "receipt",
          "documentId": "bbbbrrrrr"
        },
        {
          "documentType": "project",
          "documentId": "bbbbppppp"
        },
        {
          "documentType": "quotation",
          "documentId": "bbbbqqqqq"
        },
        {
          "documentType": "certified_exporter_authorization",
          "documentId": "currently only used for LHDNM"
        },
        {
          "documentType": "free_trade_agreement",
          "documentId": "currently only used for LHDNM"
        },
        {
          "documentType": "my_customs_form_1_9",
          "documentId": "only used for LHDNM"
        },
        {
          "documentType": "my_customs_form_2",
          "documentId": "only used for LHDNM"
        }
      ],
      "accountingCost": "23089",
      "note": "This is the invoice note. Senders can enter free text. This may not be read by the receiver, so it is not encouraged to use this.",
      "accountingSupplierParty": {
        "party": {
          "contact": {
            "email": "sender@company.com",
            "firstName": "Jony",
            "lastName": "Ponski",
            "phone": "088-333333333"
          }
        }
      },
      "accountingCustomerParty": {
        "publicIdentifiers": [
          {
            "scheme": "NL:KVK",
            "id": "27375186"
          },
          {
            "scheme": "NL:VAT",
            "id": "NL999999999B01"
          }
        ],
        "party": {
          "companyName": "Receiver Company",
          "address": {
            "street1": "Streety 123",
            "street2": null,
            "city": "Alphen aan den Rijn",
            "zip": "2400 AA",
            "county": null,
            "country": "NL"
          },
          "contact": {
            "email": "receiver@company.com",
            "firstName": "Pon",
            "lastName": "Johnson",
            "phone": "088-444444444"
          }
        }
      },
      "delivery": {
        "deliveryPartyName": "Delivered To Name",
        "actualDeliveryDate": "2020-11-01",
        "deliveryLocation": {
          "id": "871690930000478611",
          "schemeId": "EAN",
          "address": {
            "street1": "line1",
            "street2": "line2",
            "city": "CITY",
            "zip": "3423423",
            "county": "CA",
            "country": "US"
          }
        }
      },
      "paymentTerms": {
        "note": "For payment terms, only a note is supported by Peppol currently."
      },
      "paymentMeansArray": [
        {
          "code": "credit_transfer",
          "account": "NL50RABO0162432445",
          "paymentId": "44556677"
        }
      ],
      "invoiceLines": [
        {
          "lineId": "1",
          "amountExcludingVat": 2.88,
          "itemPrice": 0.12332,
          "baseQuantity": 2,
          "quantity": 63,
          "quantityUnitCode": "KWH",
          "allowanceCharges": [
            {
              "reason": "special discount",
              "amountExcludingTax": -0.25
            },
            {
              "reason": "even more special discount",
              "amountExcludingTax": -0.75
            }
          ],
          "tax": {
            "percentage": 21,
            "country": "NL",
            "category": "standard"
          },
          "orderLineReferenceLineId": "3",
          "accountingCost": "23089",
          "name": "Supply peak",
          "description": "Supply",
          "note": "Only half the story...",
          "references": [
            {
              "documentType": "item_classification_code",
              "documentId": "A reference to a commodity classification / item classification code"
            },
            {
              "documentType": "item_commodity_code",
              "documentId": "A reference to a commodity classification / commodity code"
            },
            {
              "documentType": "item_specification",
              "documentId": "A reference to an item specification document"
            },
            {
              "documentType": "line_document_reference",
              "documentId": "A reference to another document"
            },
            {
              "documentType": "line_standard_item_identification",
              "documentId": "For India IRP, use this for HSN code. For LHDNM, use this for Classification Code code."
            },
            {
              "documentType": "line_sellers_item_identification",
              "documentId": "The seller's item identification"
            },
            {
              "documentType": "line_buyers_item_identification",
              "documentId": "The buyer's item identification"
            },
            {
              "documentType": "line_purchase_order",
              "documentId": "The purchase order for this line",
              "lineId": "1",
              "issueDate": "2021-12-01"
            }
          ],
          "additionalItemProperties": [
            {
              "name": "Key1",
              "value": "871690930000222221"
            },
            {
              "name": "AnotherKey",
              "value": "VE-XXXXX"
            }
          ]
        }
      ],
      "allowanceCharges": [
        {
          "reason": "late payment",
          "amountExcludingTax": 10.2,
          "tax": {
            "percentage": 21,
            "country": "NL",
            "category": "standard"
          }
        }
      ],
      "taxSubtotals": [
        {
          "taxableAmount": 13.08,
          "taxAmount": 2.75,
          "percentage": 21,
          "country": "NL"
        }
      ],
      "amountIncludingVat": 15.83,
      "prepaidAmount": 1,
      "attachments": [
        {
          "filename": "myname.pdf",
          "document": "JVBERi0xLjIgCjkgMCBvYmoKPDwKPj4Kc3RyZWFtCkJULyAzMiBUZiggIFlPVVIgVEVYVCBIRVJFICAgKScgRVQKZW5kc3RyZWFtCmVuZG9iago0IDAgb2JqCjw8Ci9UeXBlIC9QYWdlCi9QYXJlbnQgNSAwIFIKL0NvbnRlbnRzIDkgMCBSCj4+CmVuZG9iago1IDAgb2JqCjw8Ci9LaWRzIFs0IDAgUiBdCi9Db3VudCAxCi9UeXBlIC9QYWdlcwovTWVkaWFCb3ggWyAwIDAgMjUwIDUwIF0KPj4KZW5kb2JqCjMgMCBvYmoKPDwKL1BhZ2VzIDUgMCBSCi9UeXBlIC9DYXRhbG9nCj4+CmVuZG9iagp0cmFpbGVyCjw8Ci9Sb290IDMgMCBSCj4+CiUlRU9G",
          "mimeType": "application/pdf",
          "documentId": "myId",
          "description": "A Description"
        }
      ]
    }
  }
}
  • No XML is required when invoice simulation is used
  • EN 16931 itself does not define storage or retention rules assuming that current logic with KeepInvoicesDays which is 0 by default (0 means - do not cleanup at all) should work

  • For scenario 1.1b where Admin should receive a notification about failed invoice generation, according to https://docs.portaone.com/docs/mr127-available-notification-messages it seems that we do not have such notifications even for .pdf generation. If this is indeed the case, it makes sense to add such a notification (for both - .pdf, .xml or .pdf+.xml)
  • There is a number of invoice related options on PC WI so they will need to be revised during Solution Design phase and adjusted accordingly if needed (to support e-invoicing) 

Performance / Clustering, Geo Redundancy/ Dual-Version, Porter / Call Control API

  • Consider GDPR requirements for anonymizing personal data in e-invoice (.xml file)
    • According to EN 16931, even if you anonymize PDF, the XML invoice must contain full legal data i.e. anonymization or altering XML is not allowed
  • E-invoicing (XML) must be supported for the standalone mode and dual-version setup.

ESPF 

No specific requirements for existing invoice related webhooks

Monitoring

Exact approach depends on the final design however, main point is that PortaOne support needs to understand whether e-invoice (.xml file) generation failed. For example: right now, if a .pdf file generation failed, support will see a monitoring alert for customer_statistics task