Skip to content

Commit 6cd6ec9

Browse files
authored
Merge pull request #7 from wes4m/dev
Experimental release v0.1.2 release
2 parents 0790a69 + 5ff1648 commit 6cd6ec9

File tree

9 files changed

+65
-26
lines changed

9 files changed

+65
-26
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<div align="center">
22
<br/>
33
<img src="./docs/logo.png"/>
4-
<p>v0.1.0 (experimental)</p>
4+
<p>v0.1.2 (experimental)</p>
55
<br/>
66
<br/>
77
<p>
@@ -17,7 +17,7 @@
1717
<img src="https://img.shields.io/badge/maintainer-wes4m-blue"/>
1818
</a>
1919
<a href="https://badge.fury.io/js/zatca-xml-js">
20-
<img src="https://badge.fury.io/js/zatca-xml-js.svg/?v=0.1.0"/>
20+
<img src="https://badge.fury.io/js/zatca-xml-js.svg/?v=0.1.2"/>
2121
</a>
2222
</p>
2323
</div>

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "zatca-xml-js",
3-
"version": "0.1.1",
4-
"description": "An implementation of Saudi Arabia ZATCA's E-Invocing requirements, processes, and standards.",
3+
"version": "0.1.2",
4+
"description": "An implementation of Saudi Arabia ZATCA's E-Invoicing requirements, processes, and standards.",
55
"main": "lib/index.js",
66
"files": ["lib/**/*"],
77
"scripts": {
@@ -42,7 +42,9 @@
4242
"tax",
4343
"simplified",
4444
"api",
45-
"saudi arabia"
45+
"saudi-arabia",
46+
"phase 1",
47+
"phase 2"
4648
],
4749
"author": "Wesam Alzahir (https://wes4m.io/)",
4850
"license": "MIT"

src/examples/full.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { EGS, EGSUnitInfo } from "../zatca/egs";
22
import { ZATCASimplifiedInvoiceLineItem } from "../zatca/templates/simplified_tax_invoice_template";
33
import { ZATCASimplifiedTaxInvoice } from "../zatca/ZATCASimplifiedTaxInvoice";
4-
4+
import { generatePhaseOneQR } from "../zatca/qr";
55

66
// Sample line item
77
const line_item: ZATCASimplifiedInvoiceLineItem = {
@@ -59,6 +59,7 @@ const invoice = new ZATCASimplifiedTaxInvoice({
5959

6060
const main = async () => {
6161
try {
62+
6263
// Init a new EGS
6364
const egs = new EGS(egsunit);
6465

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

7172
// Sign invoice
72-
const {signed_invoice_string, invoice_hash} = egs.signInvoice(invoice);
73+
const {signed_invoice_string, invoice_hash, qr} = egs.signInvoice(invoice);
7374

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

85+
8486
} catch (error: any) {
8587
console.log(error.message ?? error);
8688
}

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
export * from "./zatca/egs";
2-
export * from "./zatca/ZATCASimplifiedTaxInvoice";
2+
export * from "./zatca/ZATCASimplifiedTaxInvoice";
3+
export { generatePhaseOneQR } from "./zatca/qr";

src/logger/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
const LOGGING = process.env.LOGGING == "1";
3+
4+
export const log = (type: string, source: string, message: string) => {
5+
if (!LOGGING) return;
6+
console.log(`\x1b[33m${new Date().toLocaleString()}\x1b[0m: [\x1b[36m${source} ${type}\x1b[0m] ${message}`);
7+
}

src/parser/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { XMLBuilder, XmlBuilderOptions, XMLParser } from "fast-xml-parser";
22
import _ from "lodash";
3+
import { log } from "../logger";
34

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

141142
return true;
142143
} catch(error: any) {
143-
console.log(error.message);
144+
log("Info", "Parser", error.message);
144145
}
145146

146147
return false;

src/zatca/egs/index.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -235,14 +235,13 @@ export class EGS {
235235
* Signs a given invoice using the EGS certificate and keypairs.
236236
* @param invoice Invoice to sign
237237
* @param production Boolean production or compliance certificate.
238-
* @returns Promise void on success, throws error on fail.
238+
* @returns Promise void on success (signed_invoice_string: string, invoice_hash: string, qr: string), throws error on fail.
239239
*/
240-
signInvoice(invoice: ZATCASimplifiedTaxInvoice, production?: boolean): {signed_invoice_string: string, invoice_hash: string} {
240+
signInvoice(invoice: ZATCASimplifiedTaxInvoice, production?: boolean): {signed_invoice_string: string, invoice_hash: string, qr: string} {
241241
const certificate = production ? this.egs_info.production_certificate : this.egs_info.compliance_certificate;
242242
if (!certificate || !this.egs_info.private_key) throw new Error("EGS is missing a certificate/private key to sign the invoice.");
243243

244-
const {signed_invoice_string, invoice_hash} = invoice.sign(certificate, this.egs_info.private_key);
245-
return {signed_invoice_string, invoice_hash};
244+
return invoice.sign(certificate, this.egs_info.private_key);
246245
}
247246

248247

src/zatca/qr/index.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,37 @@ export const generateQR = ({invoice_xml, digital_signature, public_key, certific
5757
}
5858

5959

60+
/**
61+
* Generates a QR for phase one given an invoice.
62+
* This is a temporary function for backwards compatibility while phase two is not fully deployed.
63+
* @param invoice_xml XMLDocument.
64+
* @returns String base64 encoded QR data.
65+
*/
66+
export const generatePhaseOneQR = ({invoice_xml}: {invoice_xml: XMLDocument}): string => {
67+
68+
// Extract required tags
69+
const seller_name = invoice_xml.get("Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName")?.[0];
70+
const VAT_number = invoice_xml.get("Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID")?.[0].toString();
71+
const invoice_total = invoice_xml.get("Invoice/cac:LegalMonetaryTotal/cbc:TaxInclusiveAmount")?.[0]["#text"].toString();
72+
const VAT_total = invoice_xml.get("Invoice/cac:TaxTotal")?.[0]["cbc:TaxAmount"]["#text"].toString();
73+
74+
const issue_date = invoice_xml.get("Invoice/cbc:IssueDate")?.[0];
75+
const issue_time = invoice_xml.get("Invoice/cbc:IssueTime")?.[0];
76+
77+
const datetime = `${issue_date} ${issue_time}`;
78+
const formatted_datetime = moment(datetime).format("YYYY-MM-DDTHH:mm:ss")+"Z";
79+
80+
const qr_tlv = TLV([
81+
seller_name,
82+
VAT_number,
83+
formatted_datetime,
84+
invoice_total,
85+
VAT_total
86+
]);
87+
88+
return qr_tlv.toString("base64");
89+
}
90+
6091

6192
const TLV = (tags: any[]): Buffer => {
6293
const tlv_tags: Buffer[] = []

src/zatca/signing/index.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { XMLDocument } from "../../parser";
99
import { generateQR } from "../qr";
1010
import defaultUBLExtensions from "../templates/ubl_sign_extension_template";
1111
import defaultUBLExtensionsSignedProperties, {defaultUBLExtensionsSignedPropertiesForSigning} from "../templates/ubl_extension_signed_properties_template";
12+
import { log } from "../../logger";
1213

1314
/**
1415
* Removes (UBLExtensions (Signing), Signature Envelope, and QR data) Elements. Then canonicalizes the XML to c14n.
@@ -137,32 +138,27 @@ interface generateSignatureXMLParams {
137138
}
138139
/**
139140
* Main signing function.
140-
* Signs the invoice according to the following steps:
141-
* - Get the invoice hash
142-
* - Invoice must be cleaned up first before hashing and indentations, trailing lines, spaces must match.
143-
* - Get the certificate hash
144-
* -
145141
* @param invoice_xml XMLDocument of invoice to be signed.
146142
* @param certificate_string String signed EC certificate.
147143
* @param private_key_string String ec-secp256k1 private key;
148-
* @returns
144+
* @returns signed_invoice_string: string, invoice_hash: string, qr: string
149145
*/
150146
export const generateSignedXMLString = ({invoice_xml, certificate_string, private_key_string}: generateSignatureXMLParams):
151-
{signed_invoice_string: string, invoice_hash: string} => {
147+
{signed_invoice_string: string, invoice_hash: string, qr: string} => {
152148

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

155151
// 1: Invoice Hash
156152
const invoice_hash = getInvoiceHash(invoice_xml);
157-
console.log("Invoice hash: ", invoice_hash);
153+
log("Info", "Signer", `Invoice hash: ${invoice_hash}`);
158154

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

163159
// 3: Digital Certificate
164160
const digital_signature = createInvoiceDigitalSignature(invoice_hash, private_key_string);
165-
console.log("Digital signature: ", digital_signature);
161+
log("Info", "Signer", `Digital signature: ${digital_signature}`);
166162

167163
// 4: QR
168164
const qr = generateQR({
@@ -171,7 +167,7 @@ export const generateSignedXMLString = ({invoice_xml, certificate_string, privat
171167
public_key: cert_info.public_key,
172168
certificate_signature: cert_info.signature
173169
});
174-
console.log("QR: ", qr);
170+
log("Info", "Signer", `QR: ${qr}`);
175171

176172

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

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

211-
return {signed_invoice_string: signed_invoice_string, invoice_hash: invoice_hash };
207+
return {signed_invoice_string: signed_invoice_string, invoice_hash: invoice_hash, qr};
212208
}
213209

214210

0 commit comments

Comments
 (0)