├── .gitignore
├── src
├── Xml
│ ├── xml_invoice_note.xml
│ ├── xml_payment_note.xml
│ ├── xml_tax_totals.xml
│ ├── xml_billing_reference.xml
│ ├── xml_payment_means.xml
│ ├── xml_tax_exemption_reason.xml
│ ├── previous_hash.xml
│ ├── xml_line_item_tax_category.xml
│ ├── xml_line_item_discount.xml
│ ├── xml_qr_and_signature.xml
│ ├── xml_tax_line.xml
│ ├── xml_line_item.xml
│ ├── xml_client.xml
│ ├── xml_ubl_signed_properties.xml
│ ├── xml_ubl_signed_properties_hash.xml
│ ├── xml_to_hash_OLD.xml
│ ├── xml_to_hash.xml
│ ├── xml_signed.xml
│ └── xml_ubl_extensions.xml
├── Classes
│ ├── DocumentType.php
│ ├── InvoiceSetting.php
│ ├── InvoiceType.php
│ └── PaymentType.php
├── Contracts
│ └── InvoiceContract.php
├── Transformers
│ ├── PriceFormat.php
│ └── PublicKey.php
├── Actions
│ ├── GetXmlFileAction.php
│ ├── GetQrFromInvoice.php
│ ├── HandleResponseAction.php
│ └── PostRequestAction.php
├── Config
│ └── zatca.php
├── Helpers
│ ├── EgsSerialNumber.php
│ ├── InvoiceHelper.php
│ └── ConfigHelper.php
├── FatooraZatcaServiceProvider.php
├── Invoices
│ ├── Invoiceable.php
│ ├── B2B.php
│ └── B2C.php
├── Objects
│ ├── InvoiceItem.php
│ ├── Client.php
│ ├── Seller.php
│ ├── Setting.php
│ └── Invoice.php
├── Services
│ ├── Invoice
│ │ ├── TLVProtocolService.php
│ │ ├── SignInvoiceService.php
│ │ ├── XmlInvoiceItemsService.php
│ │ └── HashInvoiceService.php
│ ├── SettingService.php
│ ├── Settings
│ │ ├── CnfFileService.php
│ │ ├── Cert509Service.php
│ │ └── KeysService.php
│ └── ReportInvoiceService.php
└── Zatca.php
├── composer.json
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /vendor/
2 | composer.lock
3 |
--------------------------------------------------------------------------------
/src/Xml/xml_invoice_note.xml:
--------------------------------------------------------------------------------
1 | SET_INVOICE_NOTE
2 |
--------------------------------------------------------------------------------
/src/Xml/xml_payment_note.xml:
--------------------------------------------------------------------------------
1 |
2 | SET_PAYMENT_NOTE
3 |
4 |
--------------------------------------------------------------------------------
/src/Xml/xml_tax_totals.xml:
--------------------------------------------------------------------------------
1 |
2 | SET_TAX_AMOUNT
3 | SET_TAX_LINES
4 |
5 |
--------------------------------------------------------------------------------
/src/Classes/DocumentType.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Invoice Number: SET_INVOICE_NUMBER
4 |
5 |
--------------------------------------------------------------------------------
/src/Classes/InvoiceSetting.php:
--------------------------------------------------------------------------------
1 |
2 | SET_INVOICE_PAYMENT_TYPE
3 | SET_INVOICE_NOTE
4 | SET_PAYMENT_NOTE
5 |
6 |
--------------------------------------------------------------------------------
/src/Contracts/InvoiceContract.php:
--------------------------------------------------------------------------------
1 | INVOICE_TAX_EXEMPTION_REASON_CODE
2 | INVOICE_TAX_EXEMPTION_REASON
3 |
--------------------------------------------------------------------------------
/src/Classes/PaymentType.php:
--------------------------------------------------------------------------------
1 |
2 | PIH
3 |
4 | SET_PREVIOUS_INVOICE_HASH
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/Xml/xml_line_item_tax_category.xml:
--------------------------------------------------------------------------------
1 |
2 | TAX_CATEGORY_ID
3 | PERCENT_VALUE
4 |
5 | VAT
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/Xml/xml_line_item_discount.xml:
--------------------------------------------------------------------------------
1 |
2 | false
3 | ITEM_DISCOUNT_REASON
4 | ITEM_DISCOUNT_VALUE
5 |
6 |
--------------------------------------------------------------------------------
/src/Transformers/PriceFormat.php:
--------------------------------------------------------------------------------
1 |
2 | QR
3 |
4 | SET_QR_CODE_DATA
5 |
6 |
7 |
8 | urn:oasis:names:specification:ubl:signature:Invoice
9 | urn:oasis:names:specification:ubl:dsig:enveloped:xades
10 |
11 |
--------------------------------------------------------------------------------
/src/Transformers/PublicKey.php:
--------------------------------------------------------------------------------
1 | **_Note:_** This package is a demo and if you need the completed package with documentation to use it contact me on WhatsApp: [+201270115241](https://wa.me/201270115241) or [abdelrahmangamal990@gmail.com](mailto:abdelrahmangamal990@gmail.com) and we can determine the price.
11 |
--------------------------------------------------------------------------------
/src/Config/zatca.php:
--------------------------------------------------------------------------------
1 | [
12 | 'local' => env('ZATCA_LOCAL', 'https://gw-apic-gov.gazt.gov.sa/e-invoicing/developer-portal'),
13 | 'production' => env('ZATCA_PRODUCTION', 'https://gw-apic-gov.gazt.gov.sa/e-invoicing/developer-portal'),
14 | ],
15 | 'app' => [
16 | 'environment' => env('ZATCA_ENVIRONMENT', env('APP_ENV', 'local')), # local|production
17 | ],
18 |
19 | ];
20 |
--------------------------------------------------------------------------------
/src/Xml/xml_tax_line.xml:
--------------------------------------------------------------------------------
1 |
2 | INVOICE_TAXABLE_AMOUNT
3 | INVOICE_TOTAL_TAX
4 |
5 | INVOICE_TAX_CODE
6 | INVOICE_TAX_PERCENTSET_EXEMPTION_REASON_AND_CODE
7 |
8 | VAT
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/src/Helpers/EgsSerialNumber.php:
--------------------------------------------------------------------------------
1 |
2 | ITEM_ID
3 | ITEM_QTY
4 | ITEM_NET_AMOUNT
5 |
6 | ITEM_TOTAL_TAX
7 | ITEM_TOTAL_INCLUDE_TAX
8 |
9 |
10 | ITEM_NAME
11 | ITEM_TAX_CATEGORY
12 |
13 |
14 | ITEM_NET_PRICE
15 | ITEM_DISCOUNT
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/FatooraZatcaServiceProvider.php:
--------------------------------------------------------------------------------
1 | mergeConfigFrom($this->configPath, 'zatca');
24 | }
25 |
26 | /**
27 | * Bootstrap any application services.
28 | *
29 | * @return void
30 | */
31 | public function boot()
32 | {
33 | $this->publishes([
34 | $this->configPath => config_path('zatca.php'),
35 | ], 'fatoora-zatca');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Actions/GetQrFromInvoice.php:
--------------------------------------------------------------------------------
1 | registerXPathNamespace('cbc', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2');
20 |
21 | $element->registerXPathNamespace('cac', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2');
22 |
23 | $result = $element->xpath('//cac:AdditionalDocumentReference[3]//cac:Attachment//cbc:EmbeddedDocumentBinaryObject')[0];
24 |
25 | return $result;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/Actions/HandleResponseAction.php:
--------------------------------------------------------------------------------
1 | result = $result;
14 | }
15 |
16 | public function getResult(): array
17 | {
18 | return $this->result;
19 | }
20 |
21 | public function getClearedInvoice(): string
22 | {
23 | return $this->getResult()['clearedInvoice'];
24 | }
25 |
26 | public function getInvoiceHash(): string
27 | {
28 | return $this->getResult()['invoiceHash'];
29 | }
30 |
31 | public function getQr(): string
32 | {
33 | return (new GetQrFromInvoice)->handle($this->getClearedInvoice());
34 | }
35 |
36 | public function getQrImage(): string
37 | {
38 | return \SimpleSoftwareIO\QrCode\Facades\QrCode::size(300)->generate($this->getQr())->toHtml();
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/Invoices/B2B.php:
--------------------------------------------------------------------------------
1 | seller = $seller;
22 | $this->invoice = $invoice;
23 | $this->client = $client;
24 | }
25 |
26 | public static function make(Seller $seller, Invoice $invoice, Client $client): self
27 | {
28 | return new self($seller, $invoice, $client);
29 | }
30 |
31 | public function report(): self
32 | {
33 | $this->setResult(Zatca::reportStandardInvoice($this->seller, $this->invoice, $this->client));
34 | return $this;
35 | }
36 |
37 | public function calculate(): self
38 | {
39 | return $this->report();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Objects/InvoiceItem.php:
--------------------------------------------------------------------------------
1 | id = $id;
38 | $this->product_name = $product_name;
39 | $this->quantity = $quantity;
40 | $this->price = $price;
41 | $this->discount = $discount;
42 | $this->tax = $tax;
43 | $this->tax_percent = $tax_percent;
44 | $this->total = $total;
45 | $this->discount_reason = $discount_reason;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Invoices/B2C.php:
--------------------------------------------------------------------------------
1 | seller = $seller;
22 | $this->invoice = $invoice;
23 | $this->client = $client;
24 | }
25 |
26 | public static function make(Seller $seller, Invoice $invoice, Client $client = null): self
27 | {
28 | return new self($seller, $invoice, $client);
29 | }
30 |
31 | public function report(): self
32 | {
33 | $this->setResult(Zatca::reportSimplifiedInvoice($this->seller, $this->invoice, $this->client));
34 | return $this;
35 | }
36 |
37 | public function calculate(): self
38 | {
39 | $this->setResult(Zatca::calculateSimplifiedInvoice($this->seller, $this->invoice, $this->client));
40 | return $this;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/Xml/xml_client.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SET_CLIENT_VAT_NUMBER
4 |
5 |
6 | SET_CLIENT_STREET_NAME
7 | SET_CLIENT_BUILDING_NUMBER
8 | SET_CLIENT_PLOT_IDENTIFICATION
9 | SET_CLIENT_SUB_DIVISION_NAME
10 | SET_CLIENT_CITY_NAME
11 | SET_CLIENT_POSTAL_ZONE
12 |
13 |
14 | SA
15 |
16 |
17 |
18 |
19 | VAT
20 |
21 |
22 |
23 | SET_CLIENT_REGISTRATION_NAME
24 |
25 |
--------------------------------------------------------------------------------
/src/Actions/PostRequestAction.php:
--------------------------------------------------------------------------------
1 | handle($httpcode, $response);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Objects/Client.php:
--------------------------------------------------------------------------------
1 | street_name = $street_name;
38 | $this->building_number = $building_number;
39 | $this->plot_identification = $plot_identification;
40 | $this->city_subdivision_name = $city_subdivision_name;
41 | $this->city = $city;
42 | $this->country = $country;
43 | $this->postal_number = $postal_number;
44 | $this->tax_number = $tax_number;
45 | $this->registration_name = $registration_name;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/Xml/xml_ubl_signed_properties.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SET_SIGN_TIMESTAMP
4 |
5 |
6 |
7 |
8 | SET_CERTIFICATE_HASH
9 |
10 |
11 | SET_CERTIFICATE_ISSUER
12 | SET_CERTIFICATE_SERIAL_NUMBER
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Services/Invoice/TLVProtocolService.php:
--------------------------------------------------------------------------------
1 | data = $data;
32 |
33 | $this->generate();
34 | }
35 |
36 | /**
37 | * get the tlv content in base64 format.
38 | *
39 | * @return string
40 | */
41 | public function toBase64Format(): string
42 | {
43 | return base64_encode($this->tlv);
44 | }
45 |
46 | /**
47 | * generate the tlv protocol.
48 | *
49 | * @return void
50 | */
51 | protected function generate(): void
52 | {
53 | foreach($this->data as $key => $value) {
54 |
55 | $tag = $key + 1;
56 |
57 | $length = strlen($value);
58 |
59 | $this->tlv .= $this->__toHex($tag) . $this->__toHex($length) . ($value);
60 |
61 | }
62 | }
63 |
64 | /**
65 | * __toHex
66 | *
67 | * @param string $value
68 | * @return string
69 | */
70 | protected function __toHex(string $value): string
71 | {
72 | return pack("H*", sprintf("%02X", $value));
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/Xml/xml_ubl_signed_properties_hash.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | SET_SIGN_TIMESTAMP
4 |
5 |
6 |
7 |
8 | SET_CERTIFICATE_HASH
9 |
10 |
11 | SET_CERTIFICATE_ISSUER
12 | SET_CERTIFICATE_SERIAL_NUMBER
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/Objects/Seller.php:
--------------------------------------------------------------------------------
1 | registration_number = $registration_number;
50 | $this->street_name = $street_name;
51 | $this->building_number = $building_number;
52 | $this->plot_identification = $plot_identification;
53 | $this->city_sub_division = $city_sub_division;
54 | $this->city = $city;
55 | $this->country = $country;
56 | $this->postal_number = $postal_number;
57 | $this->tax_number = $tax_number;
58 | $this->registration_name = $registration_name;
59 | $this->private_key = $private_key;
60 | $this->certificate = $certificate;
61 | $this->secret = $secret;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/Objects/Setting.php:
--------------------------------------------------------------------------------
1 | otp = $otp;
59 | $this->emailAddress = $emailAddress;
60 | $this->commonName = $commonName;
61 | $this->organizationalUnitName = $organizationalUnitName;
62 | $this->organizationName = $organizationName;
63 | $this->taxNumber = $taxNumber;
64 | $this->registeredAddress = $registeredAddress;
65 | $this->businessCategory = $businessCategory;
66 | $this->egsSerialNumber = $egsSerialNumber ?? EgsSerialNumber::generate();
67 | $this->invoiceType = $invoiceType;
68 | $this->countryName = $countryName;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/Helpers/InvoiceHelper.php:
--------------------------------------------------------------------------------
1 | total - $item->tax;
18 | // return ($item['price'] * $item['quantity']) - $item['discount'];
19 | }
20 |
21 | /**
22 | * get the signing time.
23 | *
24 | * @param object $invoice
25 | * @return string
26 | */
27 | public function getSigningTime(object $invoice): string
28 | {
29 | // TODO : must send the date of signing time when post simplified invoice.
30 | return "{$invoice->invoice_date}T{$invoice->invoice_time}Z";
31 | }
32 |
33 | /**
34 | * get the timestamp of invoice.
35 | *
36 | * @param object $invoice
37 | * @return string
38 | */
39 | public function getTimestamp(object $invoice): string
40 | {
41 | return "{$invoice->invoice_date}T{$invoice->invoice_time}Z";
42 | }
43 |
44 | /**
45 | * get the hashed certificate in base64 format.
46 | * note : certificate parameter is in base64 format.
47 | *
48 | * @param mixed $certificate
49 | * @return string
50 | */
51 | public function getHashedCertificate(string $certificate): string
52 | {
53 | $certificate = base64_decode($certificate);
54 |
55 | $certificate = hash('sha256', $certificate, false);
56 |
57 | return base64_encode($certificate);
58 | }
59 |
60 | /**
61 | * get hash signed properity in base64 format.
62 | *
63 | * @param string $signed_properties
64 | * @return string
65 | */
66 | public function getHashSignedProperity(string $signed_properties): string
67 | {
68 | $signedProperties = unpack('H*', $signed_properties)['1'];
69 |
70 | $signedProperties = hash('sha256', $signedProperties, false);
71 |
72 | return base64_encode($signedProperties);
73 | }
74 |
75 | /**
76 | * get the certificate signature from certificate output.
77 | *
78 | * @param mixed $certificate_output
79 | * @return string
80 | */
81 | public function getCertificateSignature(array $certificate_output): string
82 | {
83 | $signature = unpack('H*', $certificate_output['signature'])['1'];
84 |
85 | return pack('H*', substr($signature, 2));
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/Helpers/ConfigHelper.php:
--------------------------------------------------------------------------------
1 | zatca[$config[0]][$config[1]];
71 | }
72 | // when laravel framework
73 | else {
74 | return config($key);
75 | }
76 | }
77 | elseif(function_exists('config_item')) {
78 | // when codeigniter old versions framework
79 | return config_item($key);
80 | }
81 | else {
82 | $constant = constant(strtoupper(str_replace('.', '_', $key)));
83 |
84 | if(is_null($constant)) {
85 | throw new Exception("Unhandeled config identifier!");
86 | }
87 |
88 | return $constant;
89 | }
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/Zatca.php:
--------------------------------------------------------------------------------
1 | generate();
25 | }
26 |
27 | /**
28 | * report standard invoice.
29 | *
30 | * @param \Bl\FatooraZatca\Objects\Seller $seller
31 | * @param \Bl\FatooraZatca\Objects\Invoice $invoice
32 | * @param \Bl\FatooraZatca\Objects\Client $client
33 | * @return array
34 | */
35 | public static function reportStandardInvoice(Seller $seller, Invoice $invoice, Client $client): array
36 | {
37 | if(ConfigHelper::isProduction()) {
38 | return (new ReportInvoiceService($seller, $invoice, $client))->clearance();
39 | }
40 | else {
41 | return (new ReportInvoiceService($seller, $invoice, $client))->test(DocumentType::STANDARD);
42 | }
43 | }
44 |
45 | /**
46 | * report simplified invoice.
47 | *
48 | * @param \Bl\FatooraZatca\Objects\Seller $seller
49 | * @param \Bl\FatooraZatca\Objects\Invoice $invoice
50 | * @param \Bl\FatooraZatca\Objects\Client $client
51 | * @return array
52 | */
53 | public static function reportSimplifiedInvoice(Seller $seller, Invoice $invoice, Client $client = null): array
54 | {
55 | if(ConfigHelper::isProduction()) {
56 | return (new ReportInvoiceService($seller, $invoice, $client))->reporting();
57 | }
58 | else {
59 | return (new ReportInvoiceService($seller, $invoice, $client))->test(DocumentType::SIMPILIFIED);
60 | }
61 | }
62 |
63 | /**
64 | * calculate simplified invoice.
65 | *
66 | * @param \Bl\FatooraZatca\Objects\Seller $seller
67 | * @param \Bl\FatooraZatca\Objects\Invoice $invoice
68 | * @param \Bl\FatooraZatca\Objects\Client $client
69 | * @return array
70 | */
71 | public static function calculateSimplifiedInvoice(Seller $seller, Invoice $invoice, Client $client = null): array
72 | {
73 | return (new ReportInvoiceService($seller, $invoice, $client))->calculate(DocumentType::SIMPILIFIED);
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Services/SettingService.php:
--------------------------------------------------------------------------------
1 | data = $data;
35 | }
36 |
37 | /**
38 | * generate cnf file.
39 | * generate csr request.
40 | * generate private key.
41 | * generate public key.
42 | *
43 | * @return object
44 | */
45 | public function generate(): object
46 | {
47 | $this->setUp();
48 |
49 | $this->generateCnfFile();
50 |
51 | $this->generateKeys();
52 |
53 | $this->generateCert509();
54 |
55 | return $this->settings;
56 | }
57 |
58 | /**
59 | * setUp settings data of tax payer for reporting|clearance invoices.
60 | *
61 | * @return void
62 | */
63 | protected function setUp(): void
64 | {
65 | if(! isset($this->settings)) {
66 |
67 | $this->settings = (object) [
68 | 'cnf' => null, # the cnf file
69 | 'private_key' => null, # the private key
70 | 'public_key' => null, # the public key
71 | 'csr' => null, # the certificate request
72 | 'cert_production' => null, # the certificate 509 production
73 | 'secret_production' => null, # the secret production
74 | 'csid_id_production' => null, # the csid id production
75 | 'cert_compliance' => null, # the certificate 509 compliance
76 | 'secret_compliance' => null, # the secret compliance
77 | 'csid_id_compliance' => null, # the csid id compliance
78 | ];
79 |
80 | }
81 | }
82 |
83 | /**
84 | * generate cnf file.
85 | *
86 | * @return void
87 | */
88 | protected function generateCnfFile(): void
89 | {
90 | $this->settings->cnf = (new CnfFileService($this->data))->generate();
91 | }
92 |
93 | /**
94 | * generate public & private key in base64 format.
95 | *
96 | * @return void
97 | */
98 | protected function generateKeys(): void
99 | {
100 | list(
101 |
102 | $this->settings->private_key,
103 |
104 | $this->settings->public_key,
105 |
106 | $this->settings->csr
107 |
108 | ) = (new KeysService($this->data, $this->settings->cnf))->generate();
109 | }
110 |
111 | /**
112 | * generate certificate 509 & it's data.
113 | *
114 | * @return void
115 | */
116 | protected function generateCert509(): void
117 | {
118 | (new Cert509Service($this->data))->generate($this->settings);
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/Services/Settings/CnfFileService.php:
--------------------------------------------------------------------------------
1 | data = $data;
39 | }
40 |
41 |
42 | /**
43 | * generate cnf file as base64 encode.
44 | *
45 | * @return string
46 | */
47 | public function generate(): string
48 | {
49 | $this->setCertificateTemplateName();
50 |
51 | $this->setCnfFileData();
52 |
53 | return base64_encode($this->cnf);
54 | }
55 |
56 | /**
57 | * set certificate template name.
58 | *
59 | * @return void
60 | */
61 | protected function setCertificateTemplateName(): void
62 | {
63 | $CTN = ConfigHelper::isProduction() ? 'ZATCA' : 'TSTZATCA';
64 |
65 | $this->certificateTemplateName = "{$CTN}-Code-Signing";
66 | }
67 |
68 | /**
69 | * set cnf file as string.
70 | *
71 | * @return void
72 | */
73 | protected function setCnfFileData(): void
74 | {
75 | $this->cnf = "
76 | oid_section = OIDs
77 | [ OIDs ]
78 | certificateTemplateName= 1.3.6.1.4.1.311.20.2
79 |
80 | [ req ]
81 | default_bits = 2048
82 | emailAddress = {$this->data->emailAddress}
83 | req_extensions = v3_req
84 | x509_extensions = v3_ca
85 | prompt = no
86 | default_md = sha256
87 | req_extensions = req_ext
88 | distinguished_name = dn
89 |
90 | [ v3_req ]
91 | basicConstraints = CA:FALSE
92 | keyUsage = digitalSignature, nonRepudiation, keyEncipherment
93 |
94 | [req_ext]
95 | certificateTemplateName = ASN1:PRINTABLESTRING:{$this->certificateTemplateName}
96 | subjectAltName = dirName:alt_names
97 |
98 | [ v3_ca ]
99 |
100 |
101 | # Extensions for a typical CA
102 |
103 |
104 | # PKIX recommendation.
105 |
106 | subjectKeyIdentifier=hash
107 |
108 | authorityKeyIdentifier=keyid:always,issuer:always
109 | [ dn ]
110 | CN ={$this->data->commonName} # Common Name
111 | C={$this->data->countryName} # Country Code e.g SA
112 | OU={$this->data->organizationalUnitName} # Organization Unit Name
113 | O={$this->data->organizationName} # Organization Name
114 |
115 | [ alt_names ]
116 | SN={$this->data->egsSerialNumber} # EGS Serial Number 1-ABC|2-PQR|3-XYZ
117 | UID={$this->data->taxNumber} # Organization Identifier (VAT Number)
118 | title={$this->data->invoiceType} # Invoice Type
119 | registeredAddress={$this->data->registeredAddress} # Address
120 | businessCategory={$this->data->businessCategory} # Business Category";
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/src/Xml/xml_to_hash_OLD.xml:
--------------------------------------------------------------------------------
1 | SET_XML_ENCODING
2 | SET_UBL_EXTENSIONS_FOR_SIGNED
3 | reporting:1.0
4 | SET_INVOICE_SERIAL_NUMBER
5 | SET_TERMINAL_UUID
6 | SET_ISSUE_DATE
7 | SET_ISSUE_TIME
8 | SET_INVOICE_TYPE
9 | SET_CURRENCY
10 | SET_CURRENCY
11 | SET_BILLING_REFERENCE
12 |
13 | ICV
14 | SET_INVOICE_COUNTER_NUMBER
15 |
16 |
17 | PIH
18 |
19 | SET_PREVIOUS_INVOICE_HASH
20 |
21 |
22 | SET_QR_AND_SIGNATURE_FOR_SIGNED
23 |
24 |
25 |
26 | SET_COMMERCIAL_REGISTRATION_NUMBER
27 |
28 |
29 | SET_STREET_NAME
30 | SET_BUILDING_NUMBER
31 | SET_PLOT_IDENTIFICATION
32 | SET_CITY_SUBDIVISION
33 | SET_CITY
34 | SET_POSTAL_NUMBER
35 |
36 | SET_SUPPLIER_COUNTRY
37 |
38 |
39 |
40 | SET_VAT_NUMBER
41 |
42 | VAT
43 |
44 |
45 |
46 | SET_VAT_NAME
47 |
48 |
49 |
50 |
51 | SET_CLIENT
52 |
53 | SET_PAYMENT_TYPE
54 | SET_TAX_TOTALS
55 |
56 | TOTAL_TAX_AMOUNT
57 |
58 |
59 | SET_LINE_EXTENSION_AMOUNT
60 | SET_LINE_EXTENSION_AMOUNT
61 | SET_NET_TOTAL
62 | 0
63 | 0
64 | SET_NET_TOTAL
65 |
66 | SET_INVOICE_LINES
67 |
--------------------------------------------------------------------------------
/src/Xml/xml_to_hash.xml:
--------------------------------------------------------------------------------
1 | SET_XML_ENCODING
2 | SET_UBL_EXTENSIONS_FOR_SIGNED
3 | reporting:1.0
4 | SET_INVOICE_SERIAL_NUMBER
5 | SET_TERMINAL_UUID
6 | SET_ISSUE_DATE
7 | SET_ISSUE_TIME
8 | SET_INVOICE_TYPE
9 | SET_CURRENCY
10 | SET_CURRENCY
11 | SET_BILLING_REFERENCE
12 |
13 | ICV
14 | SET_INVOICE_COUNTER_NUMBER
15 |
16 |
17 | PIH
18 |
19 | SET_PREVIOUS_INVOICE_HASH
20 |
21 |
22 | SET_QR_AND_SIGNATURE_FOR_SIGNED
23 |
24 |
25 |
26 | SET_COMMERCIAL_REGISTRATION_NUMBER
27 |
28 |
29 | SET_STREET_NAME
30 | SET_BUILDING_NUMBER
31 | SET_PLOT_IDENTIFICATION
32 | SET_CITY_SUBDIVISION
33 | SET_CITY
34 | SET_POSTAL_NUMBER
35 |
36 | SET_SUPPLIER_COUNTRY
37 |
38 |
39 |
40 | SET_VAT_NUMBER
41 |
42 | VAT
43 |
44 |
45 |
46 | SET_VAT_NAME
47 |
48 |
49 |
50 |
51 | SET_CLIENT
52 |
53 |
54 | SET_DELIVERY_DATE
55 |
56 | SET_PAYMENT_TYPE
57 | SET_TAX_TOTALS
58 |
59 | TOTAL_TAX_AMOUNT
60 |
61 |
62 | SET_LINE_EXTENSION_AMOUNT
63 | SET_LINE_EXTENSION_AMOUNT
64 | SET_NET_TOTAL
65 | 0
66 | 0
67 | SET_NET_TOTAL
68 |
69 | SET_INVOICE_LINES
70 |
--------------------------------------------------------------------------------
/src/Xml/xml_signed.xml:
--------------------------------------------------------------------------------
1 |
2 | reporting:1.0
3 | SET_INVOICE_SERIAL_NUMBER
4 | SET_TERMINAL_UUID
5 | SET_ISSUE_DATE
6 | SET_ISSUE_TIME
7 | SET_INVOICE_TYPE
8 | SET_CURRENCY
9 | SET_CURRENCY
10 | SET_BILLING_REFERENCE
11 |
12 | ICV
13 | SET_INVOICE_COUNTER_NUMBER
14 |
15 | SET_PREVIOUS_INVOICE_HASH
16 |
17 | QR
18 |
19 | SET_QR_CODE_DATA
20 |
21 |
22 |
23 | urn:oasis:names:specification:ubl:signature:Invoice
24 | urn:oasis:names:specification:ubl:dsig:enveloped:xades
25 |
26 |
27 |
28 |
29 | SET_COMMERCIAL_REGISTRATION_NUMBER
30 |
31 |
32 | SET_STREET_NAME
33 | SET_BUILDING_NUMBER
34 | SET_PLOT_IDENTIFICATION
35 | SET_CITY_SUBDIVISION
36 | SET_CITY
37 | SET_POSTAL_NUMBER
38 |
39 | SA
40 |
41 |
42 |
43 | SET_VAT_NUMBER
44 |
45 | VAT
46 |
47 |
48 |
49 | SET_VAT_NAME
50 |
51 |
52 |
53 |
54 | SET_CLIENT
55 |
56 | SET_RETURN_REASON
57 | SET_TAX_TOTALS
58 |
59 | TOTAL_TAX_AMOUNT
60 |
61 |
62 | SET_LINE_EXTENSION_AMOUNT
63 | SET_LINE_EXTENSION_AMOUNT
64 | SET_NET_TOTAL
65 | 0
66 | 0
67 | SET_NET_TOTAL
68 |
69 | SET_LINE_ITEMS
70 |
71 |
--------------------------------------------------------------------------------
/src/Objects/Invoice.php:
--------------------------------------------------------------------------------
1 |
95 | */
96 | public $invoice_items;
97 |
98 | public function __construct(
99 | int $id,
100 | string $invoice_number,
101 | string $invoice_uuid,
102 | string $invoice_date,
103 | string $invoice_time,
104 | int $invoice_type,
105 | int $payment_type,
106 | float $price,
107 | float $discount,
108 | float $tax,
109 | float $total,
110 | array $invoice_items,
111 | string $previous_hash = null,
112 | int $invoice_billing_id = null,
113 | string $invoice_note = null,
114 | string $payment_note = null,
115 | string $currency = 'SAR',
116 | float $tax_percent = 15,
117 | string $delivery_date = NULL
118 | )
119 | {
120 | $this->id = $id;
121 | $this->invoice_number = $invoice_number;
122 | $this->invoice_billing_id = $invoice_billing_id;
123 | $this->invoice_uuid = $invoice_uuid;
124 | $this->invoice_date = $invoice_date;
125 | $this->invoice_time = $invoice_time;
126 | $this->invoice_type = $invoice_type;
127 | $this->payment_type = $payment_type;
128 | $this->invoice_note = $invoice_note;
129 | $this->payment_note = $payment_note;
130 | $this->currency = $currency;
131 | $this->previous_hash = $previous_hash;
132 | $this->price = $price;
133 | $this->discount = $discount;
134 | $this->tax = $tax;
135 | $this->total = $total;
136 | $this->invoice_items = $invoice_items;
137 | $this->tax_percent = $tax_percent;
138 | $this->delivery_date = $delivery_date ?? $invoice_date;
139 | }
140 | }
141 |
--------------------------------------------------------------------------------
/src/Xml/xml_ubl_extensions.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | urn:oasis:names:specification:ubl:dsig:enveloped:xades
4 |
5 |
6 |
7 | urn:oasis:names:specification:ubl:signature:1
8 | urn:oasis:names:specification:ubl:signature:Invoice
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | not(//ancestor-or-self::ext:UBLExtensions)
17 |
18 |
19 | not(//ancestor-or-self::cac:Signature)
20 |
21 |
22 | not(//ancestor-or-self::cac:AdditionalDocumentReference[cbc:ID='QR'])
23 |
24 |
25 |
26 |
27 | SET_INVOICE_HASH
28 |
29 |
30 |
31 | SET_SIGNED_PROPERTIES_HASH
32 |
33 |
34 | SET_DIGITAL_SIGNATURE
35 |
36 |
37 | SET_CERTIFICATE_VALUE
38 |
39 |
40 |
41 |
42 | SET_CERTIFICATE_SIGNED_PROPERTIES
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
--------------------------------------------------------------------------------
/src/Services/ReportInvoiceService.php:
--------------------------------------------------------------------------------
1 | seller = $seller;
45 |
46 | $this->invoice = $invoice;
47 |
48 | $this->client = $client;
49 | }
50 |
51 | /**
52 | * share the invoice with zatca portal.
53 | *
54 | * @return array
55 | */
56 | public function reporting(): array
57 | {
58 | ConfigHelper::mustAllow('production');
59 |
60 | $route = '/invoices/reporting/single';
61 |
62 | return $this->report($route, DocumentType::SIMPILIFIED);
63 | }
64 |
65 | /**
66 | * clearance the invoice from zatca portal.
67 | *
68 | * @return array
69 | */
70 | public function clearance(): array
71 | {
72 | ConfigHelper::mustAllow('production');
73 |
74 | $route = '/invoices/clearance/single';
75 |
76 | return $this->report($route, DocumentType::STANDARD);
77 | }
78 |
79 | /**
80 | * test reporting the invoice from zatca portal.
81 | *
82 | * @param string $document_type
83 | * @return array
84 | */
85 | public function test(string $document_type): array
86 | {
87 | ConfigHelper::mustAllow('local');
88 |
89 | $route = '/compliance/invoices';
90 |
91 | return $this->report($route, $document_type);
92 | }
93 |
94 | /**
95 | * report the invoice to zatca.
96 | *
97 | * @param string $route
98 | * @param string $document_type
99 | * @return array
100 | */
101 | public function report(string $route, string $document_type): array
102 | {
103 | $calculateInvoice = $this->calculate($document_type);
104 |
105 | $USERPWD = $this->seller->certificate . ':' . $this->seller->secret;
106 |
107 | $response = (new PostRequestAction)->handle($route,
108 | [
109 | 'invoiceHash' => $calculateInvoice['invoiceHash'], # hashed invoice in base64 format
110 | 'uuid' => $this->invoice->invoice_uuid,
111 | 'invoice' => $calculateInvoice['clearedInvoice'], # signed invoice in base64 format
112 | ],
113 | [
114 | 'Content-Type: application/json',
115 | 'Accept-Language: en',
116 | 'Accept-Version: V2',
117 | 'Clearance-Status: 1'
118 | ],
119 | $USERPWD
120 | );
121 |
122 | return array_merge($response, $calculateInvoice);
123 | }
124 |
125 | /**
126 | * calculate the invoice of zatca.
127 | *
128 | * @param string $document_type
129 | * @return array
130 | */
131 | public function calculate(string $document_type): array
132 | {
133 | $hashInvoiceService = new HashInvoiceService($this->seller, $this->invoice, $this->client);
134 |
135 | $invoiceHash = $hashInvoiceService->generate($document_type);
136 |
137 | $signedXmlContent = (new SignInvoiceService(
138 | $this->seller,
139 | $this->invoice,
140 | $hashInvoiceService->getInvoiceXmlContent(),
141 | $invoiceHash
142 | ))->generate();
143 |
144 | return [
145 | 'invoiceHash' => $invoiceHash,
146 | 'clearedInvoice' => $signedXmlContent,
147 | ];
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/src/Services/Settings/Cert509Service.php:
--------------------------------------------------------------------------------
1 | data = $data;
33 | }
34 |
35 |
36 | /**
37 | * generate the certificate 509 & other data.
38 | *
39 | * @param object $settings
40 | * @return void
41 | */
42 | public function generate(object &$settings): void
43 | {
44 | $this->isProduction = ConfigHelper::isProduction();
45 |
46 | if($this->isProduction) {
47 |
48 | $this->handleProductionMode($settings);
49 |
50 | }
51 | else {
52 |
53 | $this->handleComplianceMode($settings);
54 |
55 | }
56 | }
57 |
58 | /**
59 | * when production mode.
60 | *
61 | * @param object $settings
62 | * @return void
63 | */
64 | public function handleProductionMode(object &$settings): void
65 | {
66 | $this->handleComplianceMode($settings);
67 |
68 | $this->setCert509('production', $settings);
69 | }
70 |
71 | /**
72 | * when test mode.
73 | *
74 | * @param object $settings
75 | * @return void
76 | */
77 | public function handleComplianceMode(object &$settings): void
78 | {
79 | $this->setCert509('compliance', $settings);
80 | }
81 |
82 | /**
83 | * set certificate 509 data.
84 | *
85 | * @param string $type production|compliance
86 | * @param object $settings
87 | * @return array
88 | */
89 | protected function setCert509(string $type, object &$settings): void
90 | {
91 | $data = $this->getPostData($type, $settings);
92 |
93 | $headers = $this->getHeaders();
94 |
95 | $route = $this->getRoute($type);
96 |
97 | $USERPWD = $this->getUSERPWD($type, $settings);
98 |
99 | $response = (new PostRequestAction)->handle($route, $data, $headers, $USERPWD);
100 |
101 | $settings->{"cert_{$type}"} = $response['binarySecurityToken'];
102 |
103 | $settings->{"secret_{$type}"} = $response['secret'];
104 |
105 | $settings->{"csid_id_{$type}"} = $response['requestID'];
106 | }
107 |
108 | /**
109 | * get post data of request.
110 | *
111 | * @param string $type production|compliance
112 | * @param object $settings
113 | * @return array
114 | */
115 | protected function getPostData(string $type, object $settings): array
116 | {
117 | if($type == 'production') {
118 |
119 | return [
120 | 'compliance_request_id' => $settings->csid_id_compliance
121 | ];
122 |
123 | }
124 |
125 | return [
126 | 'csr' => $settings->csr
127 | ];
128 | }
129 |
130 | /**
131 | * get headers of request.
132 | *
133 | * @return array
134 | */
135 | protected function getHeaders(): array
136 | {
137 | return [
138 | 'accept: application/json',
139 | 'Content-Type: application/json',
140 | 'otp: ' . $this->data->otp,
141 | 'Accept-Version: V2'
142 | ];
143 | }
144 |
145 | /**
146 | * get route of request.
147 | *
148 | * @param string $type
149 | * @return string
150 | */
151 | protected function getRoute(string $type): string
152 | {
153 | return ($type == 'production') ? '/production/csids' : '/compliance';
154 | }
155 |
156 | /**
157 | * get user & password for authentication.
158 | *
159 | * @param mixed $type
160 | * @param mixed $settings
161 | * @return string
162 | */
163 | protected function getUSERPWD(string $type, object $settings): string
164 | {
165 | $USERPWD = '';
166 |
167 | if($type == 'production') {
168 |
169 | $USERPWD = $settings->cert_compliance. ":" . $settings->secret_compliance;
170 |
171 | }
172 |
173 | return $USERPWD;
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/src/Services/Settings/KeysService.php:
--------------------------------------------------------------------------------
1 | data = $data;
75 |
76 | $this->cnf = $cnf;
77 | }
78 |
79 | /**
80 | * generate the private & public key.
81 | *
82 | * @return array
83 | */
84 | public function generate(): array
85 | {
86 | $this->setUpConig();
87 |
88 | $this->generateKeys();
89 |
90 | $this->generateCsr();
91 |
92 | $this->removeTmpFile();
93 |
94 | return [
95 |
96 | base64_encode($this->privateKey),
97 |
98 | base64_encode($this->publicKey),
99 |
100 | base64_encode($this->csr)
101 |
102 | ];
103 | }
104 |
105 | /**
106 | * set up configurations for generating the private key & csr.
107 | *
108 | * @return void
109 | */
110 | protected function setUpConig(): void
111 | {
112 | $this->tmp = tmpfile();
113 |
114 | fwrite($this->tmp, base64_decode($this->cnf));
115 |
116 | fseek($this->tmp, 0);
117 |
118 | $tmpFilePath = stream_get_meta_data($this->tmp)['uri'];
119 |
120 | // for generating the private & public key.
121 | $this->config = [
122 | "config" => $tmpFilePath,
123 | 'private_key_type' => OPENSSL_KEYTYPE_EC,
124 | 'curve_name' => 'secp256k1'
125 | ];
126 |
127 | // for generating the certificate request.
128 | $this->csrOptions = [
129 | 'digest_alg' => 'sha256',
130 | "req_extensions" => "req_ext",
131 | 'curve_name' => 'secp256k1',
132 | "config" => $tmpFilePath,
133 | ];
134 | }
135 |
136 | /**
137 | * generate private & public key.
138 | *
139 | * @return void
140 | */
141 | protected function generateKeys(): void
142 | {
143 | $res = openssl_pkey_new($this->config);
144 |
145 | if (!$res) {
146 |
147 | throw new Exception('ERROR: Fail to generate private key. -> ' . openssl_error_string());
148 |
149 | }
150 |
151 | // generate private key proccess.
152 | openssl_pkey_export($res, $this->privateKey , NULL, $this->config);
153 |
154 | $keyDetails = openssl_pkey_get_details($res);
155 |
156 | // generate public key proccess.
157 | $this->publicKey = $keyDetails["key"];
158 | }
159 |
160 | /**
161 | * generate the certificate request.
162 | *
163 | * @return void
164 | */
165 | protected function generateCsr(): void
166 | {
167 | $dn = [
168 | "commonName" => $this->data->commonName,
169 | "organizationalUnitName" => $this->data->organizationalUnitName,
170 | "organizationName" => $this->data->organizationName,
171 | "countryName" => $this->data->countryName,
172 | ];
173 |
174 | $csr = openssl_csr_new($dn, $this->privateKey, $this->csrOptions);
175 |
176 | openssl_csr_export($csr, $csrString);
177 |
178 | $this->csr = $csrString;
179 | }
180 |
181 | /**
182 | * remove the temporary file of cnf text.
183 | *
184 | * @return void
185 | */
186 | protected function removeTmpFile(): void
187 | {
188 | fclose($this->tmp);
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/src/Services/Invoice/SignInvoiceService.php:
--------------------------------------------------------------------------------
1 | seller = $seller;
79 |
80 | $this->invoice = $invoice;
81 |
82 | $this->invoiceXml = $invoice_xml;
83 |
84 | $this->invoiceHash = $invoice_hash;
85 | }
86 |
87 | /**
88 | * generate the signed invoice in base64 format.
89 | *
90 | * @return string
91 | */
92 | public function generate(): string
93 | {
94 | $this->setUp();
95 |
96 | $this->invoiceXml = str_replace('SET_XML_ENCODING', '', $this->invoiceXml);
97 |
98 | $this->invoiceXml = str_replace(
99 | 'SET_UBL_EXTENSIONS_FOR_SIGNED',
100 | $this->getUBLExtensions(),
101 | $this->invoiceXml
102 | );
103 |
104 | $this->invoiceXml = str_replace(
105 | 'SET_QR_AND_SIGNATURE_FOR_SIGNED',
106 | $this->getQRCodeData(),
107 | $this->invoiceXml
108 | );
109 |
110 | return base64_encode($this->invoiceXml);
111 | }
112 |
113 | /**
114 | * setUp data used in this service.
115 | *
116 | * @return void
117 | */
118 | protected function setUp(): void
119 | {
120 | $csrX509 = "-----BEGIN CERTIFICATE-----\r\n". base64_decode($this->seller->certificate) ."\r\n-----END CERTIFICATE-----";
121 |
122 | $x509 = new X509();
123 |
124 | $this->certificateOutput = $x509->loadX509($csrX509);
125 |
126 | $this->issuerName = $x509->getIssuerDN(X509::DN_STRING);
127 |
128 | $this->publicKey = (new PublicKey)->transform($x509->getPublicKey());
129 |
130 | $this->digitalSignature = $this->getDigitalSignature();
131 | }
132 |
133 | /**
134 | * get the digital signature in base64 format.
135 | *
136 | * @return string
137 | */
138 | protected function getDigitalSignature(): string
139 | {
140 | openssl_sign(
141 |
142 | base64_decode($this->invoiceHash),
143 |
144 | $signature,
145 |
146 | base64_decode($this->seller->private_key),
147 |
148 | 'sha256'
149 | );
150 |
151 | return base64_encode($signature);
152 | }
153 |
154 | /**
155 | * get the UBL extensions xml content.
156 | *
157 | * @return string
158 | */
159 | protected function getUBLExtensions(): string
160 | {
161 | $xml = GetXmlFileAction::handle('xml_ubl_extensions');
162 |
163 | $xml = str_replace('SET_INVOICE_HASH', $this->invoiceHash, $xml);
164 |
165 | list($signedProperties, $signedPropertiesHash) = $this->getSignedProperties();
166 |
167 | $xml = str_replace('SET_SIGNED_PROPERTIES_HASH', $signedPropertiesHash, $xml);
168 |
169 | $xml = str_replace('SET_DIGITAL_SIGNATURE', $this->digitalSignature, $xml);
170 |
171 | $xml = str_replace('SET_CERTIFICATE_VALUE', base64_decode($this->seller->certificate), $xml);
172 |
173 | $xml = str_replace('SET_CERTIFICATE_SIGNED_PROPERTIES', $signedProperties, $xml);
174 |
175 | return rtrim($xml, "\n");
176 | }
177 |
178 | /**
179 | * get the signed properties.
180 | *
181 | * @return array
182 | */
183 | protected function getSignedProperties(): array
184 | {
185 | $xml = GetXmlFileAction::handle('xml_ubl_signed_properties');
186 |
187 | $xml = str_replace('SET_SIGN_TIMESTAMP', (new InvoiceHelper)->getSigningTime($this->invoice), $xml);
188 |
189 | $xml = str_replace('SET_CERTIFICATE_HASH', (new InvoiceHelper)->getHashedCertificate($this->seller->certificate), $xml);
190 |
191 | $xml = str_replace('SET_CERTIFICATE_ISSUER', $this->issuerName, $xml);
192 |
193 | $issuerSerialNumber = $this->certificateOutput['tbsCertificate']['serialNumber']->toString();
194 |
195 | $xml = str_replace('SET_CERTIFICATE_SERIAL_NUMBER', $issuerSerialNumber, $xml);
196 |
197 | return [
198 |
199 | $xml,
200 |
201 | (new InvoiceHelper)->getHashSignedProperity($xml)
202 | ];
203 | }
204 |
205 | /**
206 | * get QR code data for stage 2 of zatca.
207 | *
208 | * @return string
209 | */
210 | protected function getQRCodeData(): string
211 | {
212 | $xml = GetXmlFileAction::handle('xml_qr_and_signature');
213 |
214 | $data = [
215 | $this->seller->registration_name,
216 | $this->seller->tax_number,
217 | (new InvoiceHelper)->getTimestamp($this->invoice),
218 | PriceFormat::transform($this->invoice->total),
219 | PriceFormat::transform($this->invoice->tax),
220 | $this->invoiceHash,
221 | $this->digitalSignature,
222 | $this->publicKey,
223 | (new InvoiceHelper)->getCertificateSignature($this->certificateOutput),
224 | ];
225 |
226 | $tlvEncoded = (new TLVProtocolService($data))->toBase64Format();
227 |
228 | $xml = str_replace('SET_QR_CODE_DATA', $tlvEncoded, $xml);
229 |
230 | $xml = rtrim($xml, "\n");
231 |
232 | return $xml;
233 | }
234 | }
235 |
--------------------------------------------------------------------------------
/src/Services/Invoice/XmlInvoiceItemsService.php:
--------------------------------------------------------------------------------
1 | invoice = $invoice;
35 |
36 | $this->invoiceItems = array_values($invoice->invoice_items);
37 | }
38 |
39 | /**
40 | * generate the xml of invoice items.
41 | *
42 | * @param string $invoice_content
43 | * @return void
44 | */
45 | public function generate(string &$invoice_content): void
46 | {
47 | $invoice_content = str_replace('SET_TAX_TOTALS', $this->getTaxTotalXmlContent(), $invoice_content);
48 |
49 | // ? total tax of invoice itself.
50 | $invoice_content = str_replace(
51 | 'TOTAL_TAX_AMOUNT',
52 | PriceFormat::transform($this->invoice->tax),
53 | $invoice_content
54 | );
55 |
56 | //
57 | $invoice_content = str_replace(
58 | 'SET_LINE_EXTENSION_AMOUNT',
59 | PriceFormat::transform($this->invoice->total - $this->invoice->tax),
60 | $invoice_content
61 | );
62 | $invoice_content = str_replace(
63 | 'SET_NET_TOTAL',
64 | PriceFormat::transform($this->invoice->total),
65 | $invoice_content
66 | );
67 | // $invoice_content = str_replace(
68 | // 'SET_ALLOWANCE_TOTAL_AMOUNT',
69 | // 0,
70 | // $invoice_content
71 | // );
72 |
73 | // TODO : handle multiple taxes & discounts. (must edit invoice_items).
74 | $invoice_content = str_replace('SET_INVOICE_LINES', $this->getInvoiceLineXmlContent(), $invoice_content);
75 | // dd($this->getInvoiceLineXmlContent());
76 | // dd($invoice_content);
77 | }
78 |
79 | /**
80 | * get the tax total xml content.
81 | *
82 | * @return string
83 | */
84 | protected function getTaxTotalXmlContent(): string
85 | {
86 | $xml = GetXmlFileAction::handle('xml_tax_totals');
87 |
88 | $totalTax = PriceFormat::transform($this->invoice->tax);
89 |
90 | $xml = str_replace("SET_TAX_AMOUNT", $totalTax, $xml);
91 |
92 | $xml = str_replace("SET_TAX_LINES", $this->getTaxSubtotalXmlContent(), $xml);
93 |
94 | return $xml;
95 | }
96 |
97 | /**
98 | * get the tax subtotal items xml content.
99 | *
100 | * @return string
101 | */
102 | protected function getTaxSubtotalXmlContent(): string
103 | {
104 | $taxSubtotalXml = '';
105 |
106 | $item = new InvoiceItem(
107 | 0, '', 1, $this->invoice->price, $this->invoice->discount, $this->invoice->tax,
108 | $this->invoice->tax_percent, $this->invoice->total
109 | );
110 |
111 | $taxSubtotalXmlItem = GetXmlFileAction::handle('xml_tax_line');
112 |
113 | $itemSubTotal = (new InvoiceHelper)->calculateSubTotal($item);
114 |
115 | $taxSubtotalXmlItem = str_replace(
116 | 'ITEM_SUB_TOTAL',
117 | PriceFormat::transform($itemSubTotal),
118 | $taxSubtotalXmlItem
119 | );
120 |
121 | $taxSubtotalXmlItem = str_replace(
122 | 'ITEM_TOTAL_TAX',
123 | PriceFormat::transform($item->tax),
124 | $taxSubtotalXmlItem
125 | );
126 |
127 | $taxSubtotalXmlItem = str_replace(
128 | 'SET_TAX_VALUE',
129 | PriceFormat::transform($item->tax_percent),
130 | $taxSubtotalXmlItem
131 | );
132 |
133 | $taxSubtotalXml .= $taxSubtotalXmlItem;
134 |
135 | $taxSubtotalXml = rtrim($taxSubtotalXml, '\n');
136 |
137 | return $taxSubtotalXml;
138 | }
139 |
140 | /**
141 | * get the invoice lines xml content.
142 | *
143 | * @return string
144 | */
145 | protected function getInvoiceLineXmlContent(): string
146 | {
147 | $invoiceLineXml = '';
148 |
149 | foreach($this->invoiceItems as $index => $item) {
150 |
151 | $xml = GetXmlFileAction::handle('xml_line_item');
152 |
153 | $xml = str_replace('ITEM_ID', $item->id, $xml);
154 |
155 | $xml = str_replace('ITEM_QTY', $item->quantity, $xml);
156 |
157 | $itemNetPrice = ($item->price - $item->discount) / $item->quantity;
158 |
159 | $xml = str_replace('ITEM_NET_PRICE', PriceFormat::transform($itemNetPrice), $xml);
160 |
161 | $xml = str_replace('ITEM_NAME', $item->product_name, $xml);
162 |
163 | $itemNetAmount = $item->total - $item->tax;
164 |
165 | $xml = str_replace('ITEM_NET_AMOUNT', PriceFormat::transform($itemNetAmount), $xml);
166 |
167 | $xml = str_replace('ITEM_TOTAL_TAX', PriceFormat::transform($item->tax), $xml);
168 |
169 | $xml = str_replace('ITEM_TOTAL_INCLUDE_TAX', PriceFormat::transform($item->total), $xml);
170 |
171 | $isLastItem = $index == count($this->invoiceItems);
172 |
173 | $xml = str_replace(
174 | 'ITEM_TAX_CATEGORY',
175 | $this->getClassifiedTaxCategoryXmlContent($item, $isLastItem),
176 | $xml
177 | );
178 | $xml = str_replace(
179 | 'ITEM_DISCOUNT',
180 | $this->getAllowanceChargeXmlContent($item, $isLastItem),
181 | $xml
182 | );
183 |
184 | $invoiceLineXml .= $xml;
185 |
186 | }
187 |
188 | $invoiceLineXml = rtrim($invoiceLineXml, '\n');
189 |
190 | return $invoiceLineXml;
191 | }
192 |
193 | /**
194 | * get the classified tax category xml content.
195 | *
196 | * @param \Bl\FatooraZatca\Objects\InvoiceItem $item
197 | * @param bool $new_line
198 | * @return string
199 | */
200 | protected function getClassifiedTaxCategoryXmlContent(InvoiceItem $item, bool $new_line): string
201 | {
202 | $xml = GetXmlFileAction::handle('xml_line_item_tax_category');
203 |
204 | $xml = str_replace(
205 | 'PERCENT_VALUE',
206 | PriceFormat::transform($item->tax_percent),
207 | $xml
208 | );
209 |
210 | $xml .= $new_line ? '\n' : '';
211 |
212 | return $xml;
213 | }
214 |
215 | /**
216 | * get the discount items xml content.
217 | *
218 | * @param \Bl\FatooraZatca\Objects\InvoiceItem $item
219 | * @param bool $new_line
220 | * @return string
221 | */
222 | protected function getAllowanceChargeXmlContent(InvoiceItem $item, bool $new_line): string
223 | {
224 | $xml = GetXmlFileAction::handle('xml_line_item_discount');
225 |
226 | $xml = str_replace(
227 | 'DISCOUNT_VALUE',
228 | PriceFormat::transform($item->discount),
229 | $xml
230 | );
231 |
232 | $xml = str_replace('DISCOUNT_REASON', $item->discount_reason ?? 'Discount', $xml);
233 |
234 | $xml .= $new_line ? '\n' : '';
235 |
236 | return $xml;
237 | }
238 |
239 |
240 | }
241 |
--------------------------------------------------------------------------------
/src/Services/Invoice/HashInvoiceService.php:
--------------------------------------------------------------------------------
1 | seller = $seller;
56 |
57 | $this->invoice = $invoice;
58 |
59 | $this->client = $client;
60 | }
61 |
62 | /**
63 | * generate the hash invoice.
64 | *
65 | * @param string $document_type
66 | * @return string
67 | */
68 | public function generate(string $document_type): string
69 | {
70 | $this->documentType = $document_type;
71 |
72 | $this->invoiceXml = GetXmlFileAction::handle('xml_to_hash');
73 |
74 | $this->invoiceXml = str_replace("\r", "", $this->invoiceXml);
75 |
76 | $this->xmlGenerator();
77 |
78 | // Eliminate Additional Signed Tags
79 | $invoice = str_replace('SET_XML_ENCODING', '', $this->invoiceXml);
80 |
81 | $invoice = str_replace('SET_UBL_EXTENSIONS_FOR_SIGNED', " ", $invoice);
82 |
83 | $invoice = str_replace('SET_QR_AND_SIGNATURE_FOR_SIGNED', " \n ", $invoice);
84 |
85 | $invoiceHash = hash('sha256', $invoice, true);
86 |
87 | return base64_encode($invoiceHash);
88 | }
89 |
90 | /**
91 | * get the invoice xml content.
92 | *
93 | * @return string
94 | */
95 | public function getInvoiceXmlContent(): string
96 | {
97 | return $this->invoiceXml;
98 | }
99 |
100 | /**
101 | * generate xml of hashed invoice.
102 | *
103 | * @return void
104 | */
105 | protected function xmlGenerator(): void
106 | {
107 | $this->setInvoiceDetails();
108 |
109 | $this->setInvoiceBillingReferenceIfExists();
110 |
111 | $this->setPreviousInvoiceHash();
112 |
113 | $this->setAccountingSupplierParty();
114 |
115 | $this->setAccountingCustomerParty();
116 |
117 | $this->setDeliveryDate();
118 |
119 | $this->setInvoicePaymentMeans();
120 |
121 | (new XmlInvoiceItemsService($this->invoice))->generate($this->invoiceXml);
122 |
123 | $this->invoiceXml = str_replace('SET_CURRENCY', $this->invoice->currency, $this->invoiceXml);
124 | }
125 |
126 | /**
127 | * assign xml data to the invoice content.
128 | *
129 | * @param string $tag
130 | * @param mixed $value
131 | * @return void
132 | */
133 | protected function setXmlInvoiceItem(string $tag, $value): void
134 | {
135 | $this->invoiceXml = str_replace($tag, $value, $this->invoiceXml);
136 | }
137 |
138 | /**
139 | * set invoice details xml data.
140 | * @return void
141 | */
142 | protected function setInvoiceDetails(): void
143 | {
144 | $this->setXmlInvoiceItem('SET_INVOICE_SERIAL_NUMBER', $this->invoice->invoice_number);
145 | $this->setXmlInvoiceItem('SET_TERMINAL_UUID', $this->invoice->invoice_uuid);
146 | $this->setXmlInvoiceItem('SET_ISSUE_DATE', $this->invoice->invoice_date);
147 | $this->setXmlInvoiceItem('SET_ISSUE_TIME', $this->invoice->invoice_time . 'Z');
148 | $this->setXmlInvoiceItem('SET_INVOICE_TYPE', $this->invoice->invoice_type);
149 | $this->setXmlInvoiceItem('SET_DOCUMENT', $this->documentType);
150 | $this->setXmlInvoiceItem('SET_INVOICE_COUNTER_NUMBER', $this->invoice->id);
151 | }
152 |
153 | /**
154 | * set invoice billing reference xml data.
155 | *
156 | * @return void
157 | */
158 | protected function setInvoiceBillingReferenceIfExists(): void
159 | {
160 | $billingReferenceContent = '';
161 |
162 | $billingId = $this->invoice->invoice_billing_id ?? null;
163 |
164 | if($billingId) {
165 |
166 | $billingReferenceContent = GetXmlFileAction::handle('xml_billing_reference');
167 |
168 | $billingReferenceContent = str_replace("SET_INVOICE_NUMBER", $billingId, $billingReferenceContent);
169 |
170 | $this->setXmlInvoiceItem('SET_BILLING_REFERENCE', $billingReferenceContent);
171 |
172 | }
173 |
174 | $this->setXmlInvoiceItem('SET_BILLING_REFERENCE', $billingReferenceContent);
175 | }
176 |
177 | /**
178 | * set previous invoice hash xml data.
179 | *
180 | * @return void
181 | */
182 | protected function setPreviousInvoiceHash(): void
183 | {
184 | $previousHash = $this->invoice->previous_hash;
185 |
186 | if(! $previousHash) {
187 |
188 | $previousHash = base64_encode(hash('sha256', 0,true));
189 |
190 | }
191 |
192 | $this->setXmlInvoiceItem('SET_PREVIOUS_INVOICE_HASH', $previousHash);
193 | }
194 |
195 | /**
196 | * set accounting supplier party xml data.
197 | *
198 | * @return void
199 | */
200 | protected function setAccountingSupplierParty(): void
201 | {
202 | $this->setXmlInvoiceItem('SET_COMMERCIAL_REGISTRATION_NUMBER', $this->seller->registration_number);
203 | $this->setXmlInvoiceItem('SET_STREET_NAME', $this->seller->street_name);
204 | $this->setXmlInvoiceItem('SET_BUILDING_NUMBER', $this->seller->building_number);
205 | $this->setXmlInvoiceItem('SET_PLOT_IDENTIFICATION', $this->seller->plot_identification);
206 | $this->setXmlInvoiceItem('SET_CITY_SUBDIVISION', $this->seller->city_sub_division);
207 | $this->setXmlInvoiceItem('SET_CITY', $this->seller->city);
208 | $this->setXmlInvoiceItem('SET_POSTAL_NUMBER', $this->seller->postal_number);
209 | $this->setXmlInvoiceItem('SET_SUPPLIER_COUNTRY', $this->seller->country);
210 | $this->setXmlInvoiceItem('SET_VAT_NUMBER', $this->seller->tax_number);
211 | $this->setXmlInvoiceItem('SET_VAT_NAME', $this->seller->registration_name);
212 | }
213 |
214 | /**
215 | * set accounting customer party xml data.
216 | *
217 | * @return void
218 | */
219 | protected function setAccountingCustomerParty(): void
220 | {
221 | $clientContent = '';
222 |
223 | if($this->client) {
224 |
225 | $clientContent = GetXmlFileAction::handle('xml_client');
226 |
227 | $clientContent = str_replace('SET_CLIENT_VAT_NUMBER', $this->client->tax_number, $clientContent);
228 | $clientContent = str_replace('SET_CLIENT_STREET_NAME', $this->client->street_name, $clientContent);
229 | $clientContent = str_replace('SET_CLIENT_BUILDING_NUMBER', $this->client->building_number, $clientContent);
230 | $clientContent = str_replace('SET_CLIENT_PLOT_IDENTIFICATION', $this->client->plot_identification, $clientContent);
231 | $clientContent = str_replace('SET_CLIENT_SUB_DIVISION_NAME', $this->client->city_subdivision_name, $clientContent);
232 | $clientContent = str_replace('SET_CLIENT_CITY_NAME', $this->client->city, $clientContent);
233 | $clientContent = str_replace('SET_CLIENT_COUNTRY_NAME', $this->client->country, $clientContent);
234 | $clientContent = str_replace('SET_CLIENT_POSTAL_ZONE', $this->client->postal_number, $clientContent);
235 | $clientContent = str_replace('SET_CLIENT_REGISTRATION_NAME', $this->client->registration_name, $clientContent);
236 |
237 | }
238 |
239 | $this->setXmlInvoiceItem('SET_CLIENT', $clientContent);
240 | }
241 |
242 | protected function setDeliveryDate(): void
243 | {
244 | $this->setXmlInvoiceItem('SET_DELIVERY_DATE', $this->invoice->delivery_date);
245 | }
246 |
247 | /**
248 | * set invoice payment type.
249 | * set invoice note.
250 | * set payment note.
251 | *
252 | * @return void
253 | */
254 | protected function setInvoicePaymentMeans(): void
255 | {
256 | $paymentTypeContent = GetXmlFileAction::handle('xml_payment_means');
257 |
258 | $paymentTypeContent = str_replace('SET_INVOICE_PAYMENT_TYPE', $this->invoice->payment_type, $paymentTypeContent);
259 |
260 | $invoiceNoteContent = '';
261 |
262 | $invoiceType = (int) $this->invoice->invoice_type;
263 |
264 | if(in_array($invoiceType, [InvoiceType::REFUND_INVOICE, InvoiceType::CREDIT_NOTE])) {
265 |
266 | $invoiceNoteContent = GetXmlFileAction::handle('xml_invoice_note');
267 |
268 | $reason = $this->invoice->invoice_note ?? $this->getDefaultReason($invoiceType);
269 |
270 | $invoiceNoteContent = str_replace('SET_INVOICE_NOTE', $reason, $invoiceNoteContent);
271 |
272 | }
273 |
274 | $paymentNoteContent = '';
275 |
276 | if($this->invoice->payment_note) {
277 |
278 | $paymentNoteContent = GetXmlFileAction::handle('xml_payment_note');
279 |
280 | $paymentNoteContent = str_replace('SET_PAYMENT_NOTE', $this->invoice->payment_note, $paymentNoteContent);
281 |
282 | }
283 |
284 | $paymentTypeContent = str_replace('SET_INVOICE_NOTE', $invoiceNoteContent, $paymentTypeContent);
285 |
286 | $paymentTypeContent = str_replace('SET_PAYMENT_NOTE', $paymentNoteContent, $paymentTypeContent);
287 |
288 | $this->setXmlInvoiceItem('SET_PAYMENT_TYPE', $paymentTypeContent);
289 | }
290 |
291 | /**
292 | * get default reason of [refund|credit]
293 | *
294 | * @param int $invoiceType
295 | * @return string
296 | */
297 | protected function getDefaultReason(int $invoiceType): string
298 | {
299 | if($invoiceType === InvoiceType::REFUND_INVOICE) {
300 | return 'Refund Items';
301 | }
302 | else if ($invoiceType === InvoiceType::CREDIT_NOTE) {
303 | return 'Credit Invoice';
304 | }
305 | else return '';
306 | }
307 | }
308 |
--------------------------------------------------------------------------------