I’m working on Phase Two in a simulation environment.
I can generate the CSR and the compliance CSID normally using the OTP.
I’m also able to submit invoices through the compliance invoices endpoint, and they get accepted as valid without any issues.
However, when I try to send the invoice using:
https://gw-fatoora.zatca.gov.sa/e-invoicing/simulation/invoices/reporting/single
I receive a 401 Unauthorized error.
I’m working on a multi-tenant system, and here’s the code I’m using:
public function sendToZATCA(Invoice $invoice): array
{
try {
Log::info('Sending to ZATCA API', \['invoice_id' => $invoice->id\]);
*// Set ICV and Previous Hash if not set*
if (!$invoice->icv && $this->tenant) {
$invoice->icv = $this->tenant->getNextICV();
}
if (!$invoice->previous_hash && $this->tenant) {
$invoice->previous_hash = $this->tenant->zatca_previous_invoice_hash ??
'NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ==';
}
$threshold = (float) $this->b2b_threshold;
$isB2B = (float) $invoice->total > $threshold;
Log::info('Invoice type determined', \[
'invoice_id' => $invoice->id,
'total' => $invoice->total,
'threshold' => $threshold,
'is_b2b' => $isB2B
\]);
*// Generate compliant XML*
$invoiceXML = $this->generateCompliantInvoiceXML($invoice, $isB2B);
$invoiceHash = $this->calculateInvoiceHash($invoiceXML);
*// Store hash for chaining*
$invoice->update(\['invoice_hash' => $invoiceHash\]);
if ($this->tenant) {
$this->tenant->updatePreviousInvoiceHash($invoiceHash);
}
if ($isB2B) {
return $this->clearInvoice($invoice, $invoiceXML, $invoiceHash);
} else {
return $this->reportInvoice($invoice, $invoiceXML, $invoiceHash);
}
} catch (\\Exception $e) {
Log::error('ZATCA API call failed', \[
'invoice_id' => $invoice->id,
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
\]);
return \[
'success' => false,
'error' => $e->getMessage()
\];
}
}
private function reportInvoice(Invoice $invoice, string $invoiceXML, string $invoiceHash): array
{
try {
Log::info('Reporting invoice (B2C)', \['invoice_id' => $invoice->id\]);
$invoiceBase64 = base64_encode($invoiceXML);
$payload = \[
'invoiceHash' => $invoiceHash,
'uuid' => $invoice->uuid,
'invoice' => $invoiceBase64
\];
*// التحقق من بيانات الاعتماد وتحديثها إذا لزم الأمر*
$csid = $this->csid;
$secret = $this->secret;
*// إذا لم تكن موجودة في الـ properties، احصل عليها من قاعدة البيانات*
if (empty($csid) || empty($secret)) {
if ($this->tenant) {
$csid = $this->tenant->zatca_csid;
$secret = $this->tenant->zatca_secret;
*// حدث الـ properties للمرة القادمة*
if ($csid && $secret) {
$this->csid = $csid;
$this->secret = $secret;
}
}
}
if (empty($csid) || empty($secret)) {
throw new \\Exception('ZATCA credentials not configured for this tenant');
}
*// Create Basic Auth header manually to ensure correct format*
$authHeader = 'Basic ' . base64_encode($csid . ':' . $secret);
Log::info('ZATCA Report Request Details', \[
'tenant_id' => $this->tenant?->id,
'invoice_id' => $invoice->id,
'invoice_uuid' => $invoice->uuid,
'base_url' => $this->base_url,
'csid_length' => strlen($csid),
'secret_length' => strlen($secret),
'auth_header_length' => strlen($authHeader),
'payload' => \[
'invoiceHash' => substr($payload\['invoiceHash'\], 0, 20) . '...',
'uuid' => $payload\['uuid'\],
'invoice_xml_size' => strlen($invoiceXML)
\]
\]);
*// تحديد الـ endpoint حسب البيئة*
$endpoint = $this->environment === 'simulation'
? '/compliance/invoices' *// في simulation، استخدم compliance endpoint*
: '/invoices/reporting/single'; *// في production، استخدم reporting endpoint*
Log::info('Using endpoint', \[
'environment' => $this->environment,
'endpoint' => $endpoint,
'full_url' => $this->base_url . $endpoint
\]);
$response = Http::timeout(30)
->withHeaders(\[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Accept-Version' => 'V2',
'Accept-Language' => 'en',
'Authorization' => $authHeader
\])
->post($this->base_url . $endpoint, $payload);
Log::info('ZATCA Report API response', \[
'invoice_id' => $invoice->id,
'status' => $response->status(),
'response' => $response->json()
\]);
if ($response->successful()) {
$data = $response->json();
$invoice->update(\[
'zatca_reported_at' => now(),
'zatca_status' => 'REPORTED'
\]);
return \[
'success' => true,
'type' => 'reporting',
'validation_results' => $data\['validationResults'\] ?? \[\]
\];
}
$responseData = $response->json() ?? \[\];
$error = $this->formatErrorMessage($response->status(), $responseData);
$invoice->update(\[
'zatca_status' => 'REPORTING_FAILED',
'zatca_error' => $error
\]);
return \[
'success' => false,
'error' => $error,
'status_code' => $response->status(),
'response' => $responseData
\];
} catch (\\Exception $e) {
Log::error('Invoice reporting failed', \[
'invoice_id' => $invoice->id,
'error' => $e->getMessage()
\]);
$invoice->update(\[
'zatca_status' => 'REPORTING_FAILED',
'zatca_error' => $e->getMessage()
\]);
return \[
'success' => false,
'error' => $e->getMessage()
\];
}
}
*/\*\**
*\* Clear invoice with ZATCA (B2B - Standard Invoice)*
*\*/*
private function clearInvoice(Invoice $invoice, string $invoiceXML, string $invoiceHash): array
{
try {
Log::info('Clearing invoice (B2B)', \['invoice_id' => $invoice->id\]);
$invoiceBase64 = base64_encode($invoiceXML);
$payload = \[
'invoiceHash' => $invoiceHash,
'uuid' => $invoice->uuid,
'invoice' => $invoiceBase64
\];
*// التحقق من بيانات الاعتماد وتحديثها إذا لزم الأمر*
$csid = $this->csid;
$secret = $this->secret;
*// إذا لم تكن موجودة في الـ properties، احصل عليها من قاعدة البيانات*
if (empty($csid) || empty($secret)) {
if ($this->tenant) {
$csid = $this->tenant->zatca_csid;
$secret = $this->tenant->zatca_secret;
*// حدث الـ properties للمرة القادمة*
if ($csid && $secret) {
$this->csid = $csid;
$this->secret = $secret;
}
}
}
if (empty($csid) || empty($secret)) {
throw new \\Exception('ZATCA credentials not configured for this tenant');
}
*// Create Basic Auth header manually to ensure correct format*
$authHeader = 'Basic ' . base64_encode($csid . ':' . $secret);
Log::info('ZATCA Clearance Request Details', \[
'tenant_id' => $this->tenant?->id,
'invoice_id' => $invoice->id,
'invoice_uuid' => $invoice->uuid,
'base_url' => $this->base_url,
'csid_length' => strlen($csid),
'secret_length' => strlen($secret),
'payload' => \[
'invoiceHash' => substr($payload\['invoiceHash'\], 0, 20) . '...',
'uuid' => $payload\['uuid'\],
'invoice_xml_size' => strlen($invoiceXML)
\]
\]);
*// تحديد الـ endpoint حسب البيئة*
$endpoint = $this->environment === 'simulation'
? '/compliance/invoices' *// في simulation، استخدم compliance endpoint*
: '/invoices/clearance/single'; *// في production، استخدم clearance endpoint*
Log::info('Using endpoint', \[
'environment' => $this->environment,
'endpoint' => $endpoint,
'full_url' => $this->base_url . $endpoint
\]);
$response = Http::timeout(30)
->withHeaders(\[
'Accept' => 'application/json',
'Content-Type' => 'application/json',
'Accept-Version' => 'V2',
'Accept-Language' => 'en',
'Authorization' => $authHeader
\])
->post($this->base_url . $endpoint, $payload);
Log::info('ZATCA Clear API response', \[
'invoice_id' => $invoice->id,
'status' => $response->status(),
'response' => $response->json()
\]);
if ($response->successful()) {
$data = $response->json();
$invoice->update(\[
'zatca_cleared_at' => now(),
'zatca_status' => 'CLEARED'
\]);
return \[
'success' => true,
'type' => 'clearance',
'cleared_invoice' => $data\['clearedInvoice'\] ?? null,
'validation_results' => $data\['validationResults'\] ?? \[\]
\];
}
$responseData = $response->json() ?? \[\];
$error = $this->formatErrorMessage($response->status(), $responseData);
$invoice->update(\[
'zatca_status' => 'CLEARANCE_FAILED',
'zatca_error' => $error
\]);
return \[
'success' => false,
'error' => $error,
'status_code' => $response->status(),
'response' => $responseData
\];
} catch (\\Exception $e) {
Log::error('Invoice clearance failed', \[
'invoice_id' => $invoice->id,
'error' => $e->getMessage()
\]);
$invoice->update(\[
'zatca_status' => 'CLEARANCE_FAILED',
'zatca_error' => $e->getMessage()
\]);
return \[
'success' => false,
'error' => $e->getMessage()
\];
}
}