├── .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 | --------------------------------------------------------------------------------