Skip to content

Commit

Permalink
Merge pull request #7 from wes4m/dev
Browse files Browse the repository at this point in the history
Experimental release v0.1.2 release
  • Loading branch information
wes4m authored Sep 25, 2022
2 parents 0790a69 + 5ff1648 commit 6cd6ec9
Show file tree
Hide file tree
Showing 9 changed files with 65 additions and 26 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<div align="center">
<br/>
<img src="./docs/logo.png"/>
<p>v0.1.0 (experimental)</p>
<p>v0.1.2 (experimental)</p>
<br/>
<br/>
<p>
Expand All @@ -17,7 +17,7 @@
<img src="https://img.shields.io/badge/maintainer-wes4m-blue"/>
</a>
<a href="https://badge.fury.io/js/zatca-xml-js">
<img src="https://badge.fury.io/js/zatca-xml-js.svg/?v=0.1.0"/>
<img src="https://badge.fury.io/js/zatca-xml-js.svg/?v=0.1.2"/>
</a>
</p>
</div>
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "zatca-xml-js",
"version": "0.1.1",
"description": "An implementation of Saudi Arabia ZATCA's E-Invocing requirements, processes, and standards.",
"version": "0.1.2",
"description": "An implementation of Saudi Arabia ZATCA's E-Invoicing requirements, processes, and standards.",
"main": "lib/index.js",
"files": ["lib/**/*"],
"scripts": {
Expand Down Expand Up @@ -42,7 +42,9 @@
"tax",
"simplified",
"api",
"saudi arabia"
"saudi-arabia",
"phase 1",
"phase 2"
],
"author": "Wesam Alzahir (https://wes4m.io/)",
"license": "MIT"
Expand Down
6 changes: 4 additions & 2 deletions src/examples/full.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { EGS, EGSUnitInfo } from "../zatca/egs";
import { ZATCASimplifiedInvoiceLineItem } from "../zatca/templates/simplified_tax_invoice_template";
import { ZATCASimplifiedTaxInvoice } from "../zatca/ZATCASimplifiedTaxInvoice";

import { generatePhaseOneQR } from "../zatca/qr";

// Sample line item
const line_item: ZATCASimplifiedInvoiceLineItem = {
Expand Down Expand Up @@ -59,6 +59,7 @@ const invoice = new ZATCASimplifiedTaxInvoice({

const main = async () => {
try {

// Init a new EGS
const egs = new EGS(egsunit);

Expand All @@ -69,7 +70,7 @@ const main = async () => {
const compliance_request_id = await egs.issueComplianceCertificate("123345");

// Sign invoice
const {signed_invoice_string, invoice_hash} = egs.signInvoice(invoice);
const {signed_invoice_string, invoice_hash, qr} = egs.signInvoice(invoice);

// Check invoice compliance
console.log( await egs.checkInvoiceCompliance(signed_invoice_string, invoice_hash) );
Expand All @@ -81,6 +82,7 @@ const main = async () => {
// Note: This request currently fails because ZATCA sandbox returns a constant fake production certificate
console.log( await egs.reportInvoice(signed_invoice_string, invoice_hash) );


} catch (error: any) {
console.log(error.message ?? error);
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./zatca/egs";
export * from "./zatca/ZATCASimplifiedTaxInvoice";
export * from "./zatca/ZATCASimplifiedTaxInvoice";
export { generatePhaseOneQR } from "./zatca/qr";
7 changes: 7 additions & 0 deletions src/logger/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

const LOGGING = process.env.LOGGING == "1";

export const log = (type: string, source: string, message: string) => {
if (!LOGGING) return;
console.log(`\x1b[33m${new Date().toLocaleString()}\x1b[0m: [\x1b[36m${source} ${type}\x1b[0m] ${message}`);
}
3 changes: 2 additions & 1 deletion src/parser/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { XMLBuilder, XmlBuilderOptions, XMLParser } from "fast-xml-parser";
import _ from "lodash";
import { log } from "../logger";

export interface XMLObject {[tag: string]: any};
export type XMLQueryResult = XMLObject[] | undefined;
Expand Down Expand Up @@ -140,7 +141,7 @@ export class XMLDocument {

return true;
} catch(error: any) {
console.log(error.message);
log("Info", "Parser", error.message);
}

return false;
Expand Down
7 changes: 3 additions & 4 deletions src/zatca/egs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -235,14 +235,13 @@ export class EGS {
* Signs a given invoice using the EGS certificate and keypairs.
* @param invoice Invoice to sign
* @param production Boolean production or compliance certificate.
* @returns Promise void on success, throws error on fail.
* @returns Promise void on success (signed_invoice_string: string, invoice_hash: string, qr: string), throws error on fail.
*/
signInvoice(invoice: ZATCASimplifiedTaxInvoice, production?: boolean): {signed_invoice_string: string, invoice_hash: string} {
signInvoice(invoice: ZATCASimplifiedTaxInvoice, production?: boolean): {signed_invoice_string: string, invoice_hash: string, qr: string} {
const certificate = production ? this.egs_info.production_certificate : this.egs_info.compliance_certificate;
if (!certificate || !this.egs_info.private_key) throw new Error("EGS is missing a certificate/private key to sign the invoice.");

const {signed_invoice_string, invoice_hash} = invoice.sign(certificate, this.egs_info.private_key);
return {signed_invoice_string, invoice_hash};
return invoice.sign(certificate, this.egs_info.private_key);
}


Expand Down
31 changes: 31 additions & 0 deletions src/zatca/qr/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,37 @@ export const generateQR = ({invoice_xml, digital_signature, public_key, certific
}


/**
* Generates a QR for phase one given an invoice.
* This is a temporary function for backwards compatibility while phase two is not fully deployed.
* @param invoice_xml XMLDocument.
* @returns String base64 encoded QR data.
*/
export const generatePhaseOneQR = ({invoice_xml}: {invoice_xml: XMLDocument}): string => {

// Extract required tags
const seller_name = invoice_xml.get("Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName")?.[0];
const VAT_number = invoice_xml.get("Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID")?.[0].toString();
const invoice_total = invoice_xml.get("Invoice/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount")?.[0]["#text"].toString();
const VAT_total = invoice_xml.get("Invoice/cac:TaxTotal")?.[0]["cbc:TaxAmount"]["#text"].toString();

const issue_date = invoice_xml.get("Invoice/cbc:IssueDate")?.[0];
const issue_time = invoice_xml.get("Invoice/cbc:IssueTime")?.[0];

const datetime = `${issue_date} ${issue_time}`;
const formatted_datetime = moment(datetime).format("YYYY-MM-DDTHH:mm:ss")+"Z";

const qr_tlv = TLV([
seller_name,
VAT_number,
formatted_datetime,
invoice_total,
VAT_total
]);

return qr_tlv.toString("base64");
}


const TLV = (tags: any[]): Buffer => {
const tlv_tags: Buffer[] = []
Expand Down
22 changes: 9 additions & 13 deletions src/zatca/signing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { XMLDocument } from "../../parser";
import { generateQR } from "../qr";
import defaultUBLExtensions from "../templates/ubl_sign_extension_template";
import defaultUBLExtensionsSignedProperties, {defaultUBLExtensionsSignedPropertiesForSigning} from "../templates/ubl_extension_signed_properties_template";
import { log } from "../../logger";

/**
* Removes (UBLExtensions (Signing), Signature Envelope, and QR data) Elements. Then canonicalizes the XML to c14n.
Expand Down Expand Up @@ -137,32 +138,27 @@ interface generateSignatureXMLParams {
}
/**
* Main signing function.
* Signs the invoice according to the following steps:
* - Get the invoice hash
* - Invoice must be cleaned up first before hashing and indentations, trailing lines, spaces must match.
* - Get the certificate hash
* -
* @param invoice_xml XMLDocument of invoice to be signed.
* @param certificate_string String signed EC certificate.
* @param private_key_string String ec-secp256k1 private key;
* @returns
* @returns signed_invoice_string: string, invoice_hash: string, qr: string
*/
export const generateSignedXMLString = ({invoice_xml, certificate_string, private_key_string}: generateSignatureXMLParams):
{signed_invoice_string: string, invoice_hash: string} => {
{signed_invoice_string: string, invoice_hash: string, qr: string} => {

const invoice_copy: XMLDocument = new XMLDocument(invoice_xml.toString({no_header: false}));

// 1: Invoice Hash
const invoice_hash = getInvoiceHash(invoice_xml);
console.log("Invoice hash: ", invoice_hash);
log("Info", "Signer", `Invoice hash: ${invoice_hash}`);

// 2: Certificate hash and certificate info
const cert_info = getCertificateInfo(certificate_string);
console.log("Certificate info: ", cert_info);
log("Info", "Signer", `Certificate info: ${JSON.stringify(cert_info)}`);

// 3: Digital Certificate
const digital_signature = createInvoiceDigitalSignature(invoice_hash, private_key_string);
console.log("Digital signature: ", digital_signature);
log("Info", "Signer", `Digital signature: ${digital_signature}`);

// 4: QR
const qr = generateQR({
Expand All @@ -171,7 +167,7 @@ export const generateSignedXMLString = ({invoice_xml, certificate_string, privat
public_key: cert_info.public_key,
certificate_signature: cert_info.signature
});
console.log("QR: ", qr);
log("Info", "Signer", `QR: ${qr}`);


// Set Signed properties
Expand All @@ -188,7 +184,7 @@ export const generateSignedXMLString = ({invoice_xml, certificate_string, privat
const signed_properties_bytes = Buffer.from(ubl_signature_signed_properties_xml_string_for_signing);
let signed_properties_hash = createHash("sha256").update(signed_properties_bytes).digest('hex');
signed_properties_hash = Buffer.from(signed_properties_hash).toString("base64");
console.log("Signed properites hash: ", signed_properties_hash);
log("Info", "Signer", `Signed properites hash: ${signed_properties_hash}`);

// UBL Extensions
let ubl_signature_xml_string = defaultUBLExtensions(
Expand All @@ -208,7 +204,7 @@ export const generateSignedXMLString = ({invoice_xml, certificate_string, privat
let signed_invoice_string: string = signed_invoice.toString({no_header: false});
signed_invoice_string = signedPropertiesIndentationFix(signed_invoice_string);

return {signed_invoice_string: signed_invoice_string, invoice_hash: invoice_hash };
return {signed_invoice_string: signed_invoice_string, invoice_hash: invoice_hash, qr};
}


Expand Down

0 comments on commit 6cd6ec9

Please sign in to comment.