Skip to content

Commit cf52902

Browse files
committed
feat: Add complete Invoice Mapper structure with separate mappers (Supplier, Customer, InvoiceLine, Item, Price, PaymentMeans, AdditionalDocument) and validators (InvoiceValidator, InvoiceAmountValidator) for ZATCA e-invoice generation.
1 parent cb947c6 commit cf52902

10 files changed

+1111
-0
lines changed
+76
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
namespace Saleh7\Zatca\Mappers;
3+
4+
use Saleh7\Zatca\AdditionalDocumentReference;
5+
use Saleh7\Zatca\Attachment;
6+
7+
/**
8+
* Class AdditionalDocumentMapper
9+
*
10+
* This class maps additional document reference data (from an array)
11+
* into an array of AdditionalDocumentReference objects.
12+
*/
13+
class AdditionalDocumentMapper
14+
{
15+
/**
16+
* Map additional documents data to an array of AdditionalDocumentReference objects.
17+
*
18+
* @param array $documents An array of additional document data.
19+
* Each element may contain keys:
20+
* - id: string (required)
21+
* - uuid: string (optional)
22+
* - attachment: array (optional) with keys:
23+
* - content: string
24+
* - mimeCode: string (default: 'base64')
25+
* - mimeType: string (default: 'text/plain')
26+
*
27+
* @return AdditionalDocumentReference[] Array of mapped AdditionalDocumentReference objects.
28+
*/
29+
public function mapAdditionalDocuments(array $documents): array
30+
{
31+
$additionalDocs = [];
32+
33+
foreach ($documents as $doc) {
34+
// Ensure a valid document ID is provided
35+
$docId = $doc['id'] ?? '';
36+
if (empty($docId)) {
37+
continue; // Skip documents without an ID
38+
}
39+
40+
$docRef = new AdditionalDocumentReference();
41+
$docRef->setId($docId);
42+
43+
if (isset($doc['uuid']) && !empty($doc['uuid'])) {
44+
$docRef->setUUID($doc['uuid']);
45+
}
46+
47+
// If document ID is 'PIH', map the attachment if provided.
48+
if ($docId === 'PIH' && isset($doc['attachment']) && is_array($doc['attachment'])) {
49+
$attachmentData = $doc['attachment'];
50+
$attachment = (new Attachment())
51+
->setBase64Content(
52+
$attachmentData['content'] ?? '',
53+
$attachmentData['mimeCode'] ?? 'base64',
54+
$attachmentData['mimeType'] ?? 'text/plain'
55+
);
56+
$docRef->setAttachment($attachment);
57+
}
58+
59+
$additionalDocs[] = $docRef;
60+
}
61+
62+
// Append a default additional document reference for QR code if not already present.
63+
$qrExists = false;
64+
foreach ($additionalDocs as $docRef) {
65+
if ($docRef->getId() === 'QR') {
66+
$qrExists = true;
67+
break;
68+
}
69+
}
70+
if (!$qrExists) {
71+
$additionalDocs[] = (new AdditionalDocumentReference())->setId('QR');
72+
}
73+
74+
return $additionalDocs;
75+
}
76+
}

src/Mappers/CustomerMapper.php

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
namespace Saleh7\Zatca\Mappers;
3+
4+
use Saleh7\Zatca\{
5+
TaxScheme, LegalEntity, PartyTaxScheme, Address, Party
6+
};
7+
8+
/**
9+
* Class CustomerMapper
10+
*
11+
* Maps customer data (array) to a Party object.
12+
*/
13+
class CustomerMapper
14+
{
15+
/**
16+
* Maps customer data array to a Party object.
17+
*
18+
* Expected array structure:
19+
* [
20+
* "taxScheme" => ["id" => "VAT"],
21+
* "registrationName" => "Customer Name",
22+
* "taxId" => "1234567890",
23+
* "address" => [
24+
* "street" => "Main Street",
25+
* "buildingNumber" => "123",
26+
* "subdivision" => "Subdivision",
27+
* "city" => "City Name",
28+
* "postalZone" => "12345",
29+
* "country" => "SA"
30+
* ],
31+
* "identificationId" => "UniqueCustomerId", // optional
32+
* "identificationType" => "IDType" // optional
33+
* ]
34+
*
35+
* @param array $data Customer data.
36+
* @return Party The mapped customer as a Party object.
37+
*/
38+
public function map(array $data): Party
39+
{
40+
41+
if (empty($data)) {
42+
return new Party();
43+
}
44+
// Map the TaxScheme for the customer.
45+
$taxScheme = (new TaxScheme())
46+
->setId($data['taxScheme']['id'] ?? "VAT");
47+
48+
// Map the LegalEntity for the customer.
49+
$legalEntity = (new LegalEntity())
50+
->setRegistrationName($data['registrationName'] ?? '');
51+
52+
// Map the PartyTaxScheme for the customer.
53+
$partyTaxScheme = (new PartyTaxScheme())
54+
->setTaxScheme($taxScheme)
55+
->setCompanyId($data['taxId'] ?? '');
56+
57+
// Map the Address for the customer.
58+
$address = (new Address())
59+
->setStreetName($data['address']['street'] ?? '')
60+
->setBuildingNumber($data['address']['buildingNumber'] ?? '')
61+
->setCitySubdivisionName($data['address']['subdivision'] ?? '')
62+
->setCityName($data['address']['city'] ?? '')
63+
->setPostalZone($data['address']['postalZone'] ?? '')
64+
->setCountry($data['address']['country'] ?? 'SA');
65+
66+
// Create and populate the Party object.
67+
$party = (new Party())
68+
->setLegalEntity($legalEntity)
69+
->setPartyTaxScheme($partyTaxScheme)
70+
->setPostalAddress($address);
71+
72+
// Set party identification if available.
73+
if (isset($data['identificationId'])) {
74+
$party->setPartyIdentification($data['identificationId']);
75+
if (isset($data['identificationType'])) {
76+
$party->setPartyIdentificationId($data['identificationType']);
77+
}
78+
}
79+
return $party;
80+
}
81+
}

src/Mappers/InvoiceLineMapper.php

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
<?php
2+
namespace Saleh7\Zatca\Mappers;
3+
4+
use Saleh7\Zatca\{
5+
InvoiceLine, TaxTotal
6+
};
7+
8+
/**
9+
* Class InvoiceLineMapper
10+
*
11+
* Maps invoice line data (from an array) to an array of InvoiceLine objects.
12+
*/
13+
class InvoiceLineMapper
14+
{
15+
/**
16+
* @var ItemMapper Mapper for converting item data.
17+
*/
18+
private ItemMapper $itemMapper;
19+
20+
/**
21+
* @var PriceMapper Mapper for converting price data.
22+
*/
23+
private PriceMapper $priceMapper;
24+
25+
/**
26+
* InvoiceLineMapper constructor.
27+
*
28+
* Initializes the dependent mappers.
29+
*/
30+
public function __construct()
31+
{
32+
$this->itemMapper = new ItemMapper();
33+
$this->priceMapper = new PriceMapper();
34+
}
35+
36+
/**
37+
* Map an array of invoice line data to an array of InvoiceLine objects.
38+
*
39+
* Expected input for each line:
40+
* [
41+
* 'id' => (string|int),
42+
* 'unitCode' => (string),
43+
* 'lineExtensionAmount' => (float),
44+
* 'quantity' => (int|float),
45+
* 'item' => [ ... ], // Data for item mapping.
46+
* 'price' => [ ... ], // Data for price mapping.
47+
* 'taxTotal' => [ // Data for tax total mapping.
48+
* 'taxAmount' => (float),
49+
* 'roundingAmount' => (float)
50+
* ]
51+
* ]
52+
*
53+
* @param array $lines Array of invoice lines data.
54+
* @return InvoiceLine[] Array of mapped InvoiceLine objects.
55+
*/
56+
public function mapInvoiceLines(array $lines): array
57+
{
58+
$invoiceLines = [];
59+
foreach ($lines as $line) {
60+
// Map item data using ItemMapper.
61+
$item = $this->itemMapper->map($line['item'] ?? []);
62+
// Map price data using PriceMapper.
63+
$price = $this->priceMapper->map($line['price'] ?? []);
64+
// Map line tax total data.
65+
$taxTotal = $this->mapLineTaxTotal($line['taxTotal'] ?? []);
66+
// Create and populate the InvoiceLine object.
67+
$invoiceLine = (new InvoiceLine())
68+
->setUnitCode($line['unitCode'] ?? "PCE")
69+
->setId((string)($line['id'] ?? '1'))
70+
->setItem($item)
71+
->setLineExtensionAmount($line['lineExtensionAmount'] ?? 0)
72+
->setPrice($price)
73+
->setTaxTotal($taxTotal)
74+
->setInvoicedQuantity($line['quantity'] ?? 0);
75+
$invoiceLines[] = $invoiceLine;
76+
}
77+
return $invoiceLines;
78+
}
79+
80+
/**
81+
* Map line tax total data to a TaxTotal object.
82+
*
83+
* Expected input:
84+
* [
85+
* 'taxAmount' => (float),
86+
* 'roundingAmount' => (float)
87+
* ]
88+
*
89+
* @param array $data Array of line tax total data.
90+
* @return TaxTotal The mapped TaxTotal object.
91+
*/
92+
private function mapLineTaxTotal(array $data): TaxTotal
93+
{
94+
return (new TaxTotal())
95+
->setTaxAmount($data['taxAmount'] ?? 0)
96+
->setRoundingAmount($data['roundingAmount'] ?? 0);
97+
}
98+
}

0 commit comments

Comments
 (0)