Skip to content

Commit bf0a71e

Browse files
committed
test: add comprehensive unit and integration tests for invoice mappers and validators
1 parent 9755451 commit bf0a71e

5 files changed

+684
-7
lines changed

src/Mappers/InvoiceMapper.php

+9-7
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
use Saleh7\Zatca\{
66
Invoice, UBLExtensions, Signature, InvoiceType, TaxTotal, LegalMonetaryTotal, Delivery, AllowanceCharge
77
};
8-
// use Saleh7\Zatca\Mappers\Validators\InvoiceValidator;
8+
use Saleh7\Zatca\Mappers\Validators\InvoiceValidator;
99
// use Saleh7\Zatca\Mappers\Validators\InvoiceAmountValidator;
1010

1111
/**
@@ -81,12 +81,14 @@ public function mapToInvoice(array|string $data): Invoice
8181
}
8282
}
8383

84-
// Optionally, validate the required invoice fields here.
85-
// $validator = new InvoiceValidator();
86-
// $validator->validate($data);
87-
// $validator2 = new InvoiceAmountValidator();
88-
// $validator2->validateMonetaryTotals($data);
89-
// $validator2->validateInvoiceLines($data['invoiceLines']);
84+
// Optionally, Validate the required invoice fields
85+
$validator = new InvoiceValidator();
86+
$validator->validate($data);
87+
88+
// // Optionally, validate the invoice amounts and lines.
89+
// $validatorAmount = new InvoiceAmountValidator();
90+
// $validatorAmount->validateMonetaryTotals($data);
91+
// $validatorAmount->validateInvoiceLines($data['invoiceLines']);
9092

9193
$invoice = new Invoice();
9294

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<?php
2+
use PHPUnit\Framework\TestCase;
3+
use Saleh7\Zatca\Mappers\InvoiceMapper;
4+
use Saleh7\Zatca\GeneratorInvoice;
5+
/**
6+
* Class InvoiceIntegrationTest
7+
*
8+
* This integration test verifies the complete invoice generation flow:
9+
* - Mapping invoice data to an Invoice object via InvoiceMapper.
10+
* - Validating invoice data using the internal validators.
11+
* - Generating the XML output from the Invoice object.
12+
*
13+
* The test ensures that all components work together correctly.
14+
*/
15+
class InvoiceIntegrationTest extends TestCase
16+
{
17+
/**
18+
* Test the complete invoice generation flow.
19+
*
20+
* This test passes a full invoice data array, then:
21+
* 1. Maps the data to an Invoice object.
22+
* 2. Generates XML using the GeneratorInvoice.
23+
*/
24+
public function testCompleteInvoiceGenerationFlow(): void
25+
{
26+
// Sample invoice data with all required fields and valid monetary calculations.
27+
$invoiceData = [
28+
'uuid' => '3cf5ee18-ee25-44ea-a444-2c37ba7f28be',
29+
'id' => 'INV-12345',
30+
'issueDate' => '2024-09-07 17:41:08',
31+
'issueTime' => '2024-09-07 17:41:08',
32+
'currencyCode' => 'SAR',
33+
'taxCurrencyCode' => 'SAR',
34+
'invoiceType' => [
35+
'invoice' => 'simplified', // simplified invoice type
36+
'type' => 'invoice',
37+
'isThirdParty' => false,
38+
'isNominal' => false,
39+
'isExport' => false,
40+
'isSummary' => false,
41+
'isSelfBilled' => false
42+
],
43+
'additionalDocuments' => [
44+
[
45+
'id' => 'ICV',
46+
'uuid' => '10'
47+
],
48+
[
49+
'id' => 'PIH',
50+
'attachment' => [
51+
'content' => 'dummyBase64Content',
52+
'mimeCode' => 'base64',
53+
'mimeType' => 'text/plain'
54+
]
55+
]
56+
],
57+
'supplier' => [
58+
'registrationName' => 'Supplier Inc.',
59+
'taxId' => '1234567890',
60+
'identificationId' => 'SUPPLIER-001',
61+
'identificationType' => 'CRN',
62+
'taxScheme' => ['id' => 'VAT'],
63+
'address' => [
64+
'street' => 'Main St',
65+
'buildingNumber' => '123',
66+
'subdivision' => 'Al-Murooj',
67+
'city' => 'Riyadh',
68+
'postalZone' => '12345',
69+
'country' => 'SA'
70+
]
71+
],
72+
'customer' => [
73+
'registrationName' => 'Customer LLC',
74+
'taxId' => '0987654321',
75+
'taxScheme' => ['id' => 'VAT'],
76+
'address' => [
77+
'street' => 'Second St',
78+
'buildingNumber' => '456',
79+
'subdivision' => 'Al-Murooj',
80+
'city' => 'Jeddah',
81+
'postalZone' => '54321',
82+
'country' => 'SA'
83+
]
84+
],
85+
'paymentMeans' => [
86+
'code' => '10'
87+
],
88+
'allowanceCharges' => [
89+
[
90+
'isCharge' => false,
91+
'reason' => 'discount',
92+
'amount' => 0.00,
93+
'taxCategories' => [
94+
[
95+
'percent' => 15,
96+
'taxScheme' => ['id' => 'VAT']
97+
]
98+
]
99+
]
100+
],
101+
'taxTotal' => [
102+
'taxAmount' => 30,
103+
'subTotals' => [
104+
[
105+
'taxableAmount' => 200,
106+
'taxAmount' => 30,
107+
'percent' => 15,
108+
'taxScheme' => ['id' => 'VAT']
109+
]
110+
]
111+
],
112+
'legalMonetaryTotal' => [
113+
'lineExtensionAmount' => 200,
114+
'taxExclusiveAmount' => 200,
115+
'taxInclusiveAmount' => 230,
116+
'payableAmount' => 230,
117+
'allowanceTotalAmount' => 0
118+
],
119+
'invoiceLines' => [
120+
[
121+
'id' => 1,
122+
'unitCode' => 'PCE',
123+
'quantity' => 2,
124+
'lineExtensionAmount' => 200, // Calculated: 100 * 2 = 200
125+
'item' => [
126+
'name' => 'Product A',
127+
'taxPercent' => 15,
128+
'taxScheme' => ['id' => 'VAT']
129+
],
130+
'price' => [
131+
'amount' => 100,
132+
'unitCode' => 'PCE'
133+
],
134+
'taxTotal' => [
135+
'taxAmount' => 30,
136+
'roundingAmount' => 230 // Calculated: 200 + 30 = 230
137+
]
138+
]
139+
]
140+
];
141+
142+
// Map the invoice data to an Invoice object
143+
$mapper = new InvoiceMapper();
144+
$invoice = $mapper->mapToInvoice($invoiceData);
145+
$this->assertNotNull($invoice, 'Invoice object should not be null.');
146+
147+
// Generate XML output from the Invoice object
148+
$xmlOutput = GeneratorInvoice::invoice($invoice)->getXML();
149+
$this->assertNotEmpty($xmlOutput, 'Generated XML should not be empty.');
150+
}
151+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
<?php
2+
use PHPUnit\Framework\TestCase;
3+
use Saleh7\Zatca\Mappers\Validators\InvoiceAmountValidator;
4+
5+
/**
6+
* Class InvoiceAmountValidatorTest
7+
*
8+
* This class tests the InvoiceAmountValidator to ensure that the monetary totals
9+
* and invoice lines amounts are calculated correctly and consistently.
10+
*/
11+
class InvoiceAmountValidatorTest extends TestCase
12+
{
13+
/**
14+
* @var InvoiceAmountValidator
15+
*/
16+
private InvoiceAmountValidator $validator;
17+
18+
/**
19+
* Setup before each test.
20+
*/
21+
protected function setUp(): void
22+
{
23+
$this->validator = new InvoiceAmountValidator();
24+
}
25+
26+
/**
27+
* Test that valid monetary totals and invoice line amounts pass validation.
28+
*
29+
* This test uses a sample invoice data array with valid legal monetary totals and
30+
* invoice line calculations. It asserts that no exceptions are thrown.
31+
*/
32+
public function testValidAmounts(): void
33+
{
34+
$data = [
35+
'legalMonetaryTotal' => [
36+
'lineExtensionAmount' => 200,
37+
'taxExclusiveAmount' => 200,
38+
'taxInclusiveAmount' => 230,
39+
'payableAmount' => 230
40+
],
41+
'taxTotal' => [
42+
'taxAmount' => 30,
43+
'subTotals' => [
44+
[
45+
'taxableAmount' => 200,
46+
'taxAmount' => 30,
47+
'percent' => 15,
48+
'taxScheme' => ['id' => 'VAT']
49+
]
50+
]
51+
],
52+
'invoiceLines' => [
53+
[
54+
'id' => 1,
55+
'unitCode' => 'PCE',
56+
'quantity' => 2,
57+
'lineExtensionAmount'=> 200, // 100 * 2 = 200
58+
'item' => [
59+
'name' => 'Product A',
60+
'taxPercent' => 15,
61+
'taxScheme' => ['id' => 'VAT']
62+
],
63+
'price' => [
64+
'amount' => 100,
65+
'unitCode' => 'PCE'
66+
],
67+
'taxTotal' => [
68+
'taxAmount' => 30,
69+
'roundingAmount'=> 230 // 200 + 30 = 230
70+
]
71+
]
72+
]
73+
];
74+
75+
// Expect no exception during validation.
76+
$this->validator->validateMonetaryTotals($data);
77+
$this->validator->validateInvoiceLines($data['invoiceLines']);
78+
79+
$this->assertTrue(true); // Test passes if no exception is thrown.
80+
}
81+
82+
/**
83+
* Test that an incorrect lineExtensionAmount calculation throws an exception.
84+
*
85+
* For example, if price * quantity does not equal the provided lineExtensionAmount.
86+
*/
87+
public function testInvalidLineExtensionCalculationThrowsException(): void
88+
{
89+
$data = [
90+
'invoiceLines' => [
91+
[
92+
'id' => 1,
93+
'unitCode' => 'PCE',
94+
'quantity' => 2,
95+
// Incorrect lineExtensionAmount: expected 100 * 2 = 200, but provided 190
96+
'lineExtensionAmount'=> 190,
97+
'item' => [
98+
'name' => 'Product A',
99+
'taxPercent' => 15,
100+
'taxScheme' => ['id' => 'VAT']
101+
],
102+
'price' => [
103+
'amount' => 100,
104+
'unitCode' => 'PCE'
105+
],
106+
'taxTotal' => [
107+
'taxAmount' => 30,
108+
'roundingAmount'=> 220 // 190 + 30 = 220
109+
]
110+
]
111+
]
112+
];
113+
114+
$this->expectException(\InvalidArgumentException::class);
115+
$this->expectExceptionMessage("lineExtensionAmount is incorrect");
116+
$this->validator->validateInvoiceLines($data['invoiceLines']);
117+
}
118+
119+
/**
120+
* Test that an incorrect roundingAmount calculation throws an exception.
121+
*
122+
* For example, if roundingAmount does not equal lineExtensionAmount plus taxTotal.taxAmount.
123+
*/
124+
public function testInvalidRoundingAmountCalculationThrowsException(): void
125+
{
126+
$data = [
127+
'invoiceLines' => [
128+
[
129+
'id' => 1,
130+
'unitCode' => 'PCE',
131+
'quantity' => 2,
132+
'lineExtensionAmount'=> 200, // 100 * 2 = 200
133+
'item' => [
134+
'name' => 'Product A',
135+
'taxPercent' => 15,
136+
'taxScheme' => ['id' => 'VAT']
137+
],
138+
'price' => [
139+
'amount' => 100,
140+
'unitCode' => 'PCE'
141+
],
142+
'taxTotal' => [
143+
'taxAmount' => 30,
144+
// Incorrect roundingAmount: expected 200 + 30 = 230, but provided 225
145+
'roundingAmount'=> 225
146+
]
147+
]
148+
]
149+
];
150+
151+
$this->expectException(\InvalidArgumentException::class);
152+
$this->expectExceptionMessage("roundingAmount is incorrect");
153+
$this->validator->validateInvoiceLines($data['invoiceLines']);
154+
}
155+
}

0 commit comments

Comments
 (0)