دمج API TTN في نظامك: دليل المطوّر التقني

فريق نقطة
بواسطة فريق نقطة ·

جاري تحميل مشغل تحويل النص إلى كلام الصوتي...

هذا الدليل موجَّه للمطورين والمهندسين المعماريين التقنيين المكلّفين بدمج الفوترة الإلكترونية TTN في تطبيق قائم. نتناول فيه البنية الكاملة، والاستراتيجيات الثلاث للتكامل، ونقدم أمثلة كود ملموسة بـ Node.js/TypeScript لكل مرحلة رئيسية: توليد XML TEIF، التوقيع TUNTRUST، الإرسال عبر API وإدارة الاستجابات.

هذا الدليل هو الحلقة السابعة من سلسلتنا الفاتورة الإلكترونية في تونس. ويفترض أنك قرأت مسبقًا الحلقة الخامسة: تنسيق TEIF والمواصفات التقنية وأن مؤسستك مسجّلة على منصة الفاتورة.


البنية العامة للنظام

قبل كتابة أول سطر كود، من الضروري فهم كيفية ارتباط نظامك ببنيات TTN والمديرية العامة للضرائب.

┌─────────────────────────────────────────────────┐
│              نظامك الخاص                         │
│  ┌──────────┐   ┌──────────┐   ┌─────────────┐  │
│  │  ERP/CRM │──►│ مُولِّد  │──►│  وحدة TTN   │  │
│  │  (البيانات)  │ XML TEIF  │   │  (التوقيع   │  │
│  └──────────┘   └──────────┘   │  + الإرسال) │  │
│                                 └──────┬────────┘  │
└────────────────────────────────────────│───────────┘
                                         │ HTTPS/REST
                                         ▼
                          ┌──────────────────────────┐
                          │   API TTN (الفاتورة)      │
                          │  api.elfatoora.tn         │
                          │  ┌──────────────────────┐ │
                          │  │ تحقق TEIF             │ │
                          │  │ تحقق التوقيع          │ │
                          │  │ توثيق زمني TTN        │ │
                          │  │ أرشفة                 │ │
                          │  └──────────┬───────────┘ │
                          └─────────────│──────────────┘
                                        │ إرسال تلقائي
                                        ▼
                          ┌──────────────────────────┐
                          │   DGI (الضرائب)           │
                          │  تصريح TVA               │
                          │  متابعة جبائية           │
                          └──────────────────────────┘

نقاط التفاعل الرئيسية:

  • نظامك يُولّد XML TEIF من بياناتك التجارية
  • التوقيع الرقمي بشهادة TUNTRUST يُنفَّذ على جانب العميل (خادمك)
  • API TTN تتحقق من التنسيق والتوقيع وتُؤرشف الفاتورة
  • تُرسل TTN البيانات إلى DGI تلقائيًا — لا داعي لأي تفاعل مباشر مع DGI لكل فاتورة

الإعداد والمصادقة

المتطلبات التقنية المسبقة

قبل الشروع في التكامل، تأكد من توفر:

  • Node.js v18+ (يوصى بـ v20)
  • شهادة PKCS#12 (.p12) صادرة عن TUNTRUST مع كلمة مرورها
  • بيانات اعتماد API TTN (client_id وclient_secret) تُحصَل عليها بعد التسجيل في وضع EDI
  • الوصول إلى sandbox TTN: https://api-sandbox.elfatoora.tn

تثبيت الاعتماديات

npm install xmlbuilder2 node-forge axios dotenv qrcode
npm install --save-dev @types/node typescript tsx
  • xmlbuilder2: بناء وثائق XML
  • node-forge: التعامل مع شهادات PKCS#12 والتوقيع XML
  • axios: عميل HTTP لـ API TTN
  • qrcode: توليد رموز QR

متغيرات البيئة

# .env
TTN_API_BASE_URL=https://api-sandbox.elfatoora.tn
TTN_CLIENT_ID=your_ttn_client_id
TTN_CLIENT_SECRET=your_ttn_client_secret
 
# شهادة TUNTRUST
TUNTRUST_CERT_PATH=/path/to/your-certificate.p12
TUNTRUST_CERT_PASSWORD=your_certificate_password
 
# معلومات المُصدِر
SUPPLIER_TAX_ID=12345678A000000
SUPPLIER_NAME=شركتك ذ.م.م

الخيار 1: التكامل المباشر مع API TTN

التكامل المباشر هو الحل الأكثر مرونة لكنه الأكثر تعقيدًا في التنفيذ. تُدير بالكامل دورة حياة الفاتورة: توليد XML، التوقيع، الإرسال وإدارة الاستجابات.

الخطوة 1: المصادقة OAuth2

تستخدم API TTN بروتوكول OAuth2 بتدفق client_credentials:

// src/ttn/auth.ts
import axios from "axios";
 
interface TTNTokenResponse {
  access_token: string;
  token_type: string;
  expires_in: number;
}
 
let cachedToken: { token: string; expiresAt: number } | null = null;
 
export async function getTTNAccessToken(): Promise<string> {
  // إعادة استخدام الرمز إن كان لا يزال صالحًا (مع هامش 60 ثانية)
  if (cachedToken && Date.now() < cachedToken.expiresAt - 60000) {
    return cachedToken.token;
  }
 
  const response = await axios.post<TTNTokenResponse>(
    `${process.env.TTN_API_BASE_URL}/api/v1/auth/token`,
    new URLSearchParams({
      grant_type: "client_credentials",
      client_id: process.env.TTN_CLIENT_ID!,
      client_secret: process.env.TTN_CLIENT_SECRET!,
      scope: "invoices:write invoices:read",
    }),
    { headers: { "Content-Type": "application/x-www-form-urlencoded" } }
  );
 
  cachedToken = {
    token: response.data.access_token,
    expiresAt: Date.now() + response.data.expires_in * 1000,
  };
 
  return cachedToken.token;
}

الخطوة 2: توليد XML TEIF

// src/ttn/teif-generator.ts
import { create } from "xmlbuilder2";
 
export interface InvoiceLine {
  id: number;
  description: string;
  quantity: number;
  unitCode: string;
  unitPrice: number; // HT
  vatRate: number;   // مثال: 19 لـ 19%
}
 
export interface InvoiceData {
  invoiceNumber: string;
  issueDate: string;       // YYYY-MM-DD
  dueDate: string;
  supplierTaxId: string;
  supplierName: string;
  supplierAddress: string;
  supplierCity: string;
  supplierPostalCode: string;
  customerTaxId: string;
  customerName: string;
  customerAddress: string;
  customerCity: string;
  customerPostalCode: string;
  lines: InvoiceLine[];
  notes?: string;
}
 
function formatAmount(amount: number): string {
  return amount.toFixed(3);
}
 
export function generateTEIF(invoice: InvoiceData): string {
  // حساب الإجماليات
  const lineCalculations = invoice.lines.map((line) => {
    const lineTotal = line.quantity * line.unitPrice;
    const vatAmount = (lineTotal * line.vatRate) / 100;
    return { ...line, lineTotal, vatAmount };
  });
 
  const subtotalHT = lineCalculations.reduce((s, l) => s + l.lineTotal, 0);
  const totalVAT = lineCalculations.reduce((s, l) => s + l.vatAmount, 0);
  const totalTTC = subtotalHT + totalVAT;
 
  // بناء وثيقة XML
  const doc = create({ version: "1.0", encoding: "UTF-8" })
    .ele("Invoice", {
      xmlns: "urn:tn:gov:dgi:teif:1.8",
      "xmlns:xsi": "http://www.w3.org/2001/XMLSchema-instance",
      "xsi:schemaLocation":
        "urn:tn:gov:dgi:teif:1.8 TEIF_v1.8.7.xsd",
    });
 
  // Header
  const header = doc.ele("Header");
  header.ele("InvoiceID").txt(invoice.invoiceNumber);
  header.ele("IssueDate").txt(invoice.issueDate);
  header.ele("IssueTime").txt(new Date().toTimeString().split(" ")[0]);
  header.ele("InvoiceTypeCode").txt("380");
  header.ele("DocumentCurrencyCode").txt("TND");
  header.ele("TaxCurrencyCode").txt("TND");
  header.ele("DueDate").txt(invoice.dueDate);
  if (invoice.notes) {
    header.ele("Note").txt(invoice.notes);
  }
 
  // الأطراف
  const parties = doc.ele("Parties");
 
  const supplier = parties.ele("Supplier");
  supplier.ele("PartyIdentification").ele("ID", { schemeID: "TN_MF" })
    .txt(invoice.supplierTaxId);
  supplier.ele("PartyName").ele("Name").txt(invoice.supplierName);
  const supplierAddr = supplier.ele("PostalAddress");
  supplierAddr.ele("StreetName").txt(invoice.supplierAddress);
  supplierAddr.ele("CityName").txt(invoice.supplierCity);
  supplierAddr.ele("PostalZone").txt(invoice.supplierPostalCode);
  supplierAddr.ele("Country").ele("IdentificationCode").txt("TN");
 
  const customer = parties.ele("Customer");
  customer.ele("PartyIdentification").ele("ID", { schemeID: "TN_MF" })
    .txt(invoice.customerTaxId);
  customer.ele("PartyName").ele("Name").txt(invoice.customerName);
  const customerAddr = customer.ele("PostalAddress");
  customerAddr.ele("StreetName").txt(invoice.customerAddress);
  customerAddr.ele("CityName").txt(invoice.customerCity);
  customerAddr.ele("PostalZone").txt(invoice.customerPostalCode);
  customerAddr.ele("Country").ele("IdentificationCode").txt("TN");
 
  // بنود الفاتورة
  const lines = doc.ele("InvoiceLines");
  for (const line of lineCalculations) {
    const invoiceLine = lines.ele("InvoiceLine");
    invoiceLine.ele("ID").txt(String(line.id));
    invoiceLine.ele("InvoicedQuantity", { unitCode: line.unitCode })
      .txt(String(line.quantity));
    invoiceLine.ele("LineExtensionAmount", { currencyID: "TND" })
      .txt(formatAmount(line.lineTotal));
 
    const item = invoiceLine.ele("Item");
    item.ele("Name").txt(line.description);
    const taxCat = item.ele("ClassifiedTaxCategory");
    taxCat.ele("ID").txt("S");
    taxCat.ele("Percent").txt(String(line.vatRate));
    taxCat.ele("TaxScheme").ele("ID").txt("TVA");
 
    const price = invoiceLine.ele("Price");
    price.ele("PriceAmount", { currencyID: "TND" })
      .txt(formatAmount(line.unitPrice));
    price.ele("BaseQuantity", { unitCode: line.unitCode }).txt("1");
  }
 
  // إجماليات TVA
  const taxTotal = doc.ele("TaxTotal");
  taxTotal.ele("TaxAmount", { currencyID: "TND" }).txt(formatAmount(totalVAT));
  const taxSubtotal = taxTotal.ele("TaxSubtotal");
  taxSubtotal.ele("TaxableAmount", { currencyID: "TND" })
    .txt(formatAmount(subtotalHT));
  taxSubtotal.ele("TaxAmount", { currencyID: "TND" })
    .txt(formatAmount(totalVAT));
  const taxCategory = taxSubtotal.ele("TaxCategory");
  taxCategory.ele("ID").txt("S");
  taxCategory.ele("Percent").txt("19");
  taxCategory.ele("TaxScheme").ele("ID").txt("TVA");
 
  // المبالغ الإجمالية
  const monetary = doc.ele("LegalMonetaryTotal");
  monetary.ele("LineExtensionAmount", { currencyID: "TND" })
    .txt(formatAmount(subtotalHT));
  monetary.ele("TaxExclusiveAmount", { currencyID: "TND" })
    .txt(formatAmount(subtotalHT));
  monetary.ele("TaxInclusiveAmount", { currencyID: "TND" })
    .txt(formatAmount(totalTTC));
  monetary.ele("PayableAmount", { currencyID: "TND" })
    .txt(formatAmount(totalTTC));
 
  return doc.end({ prettyPrint: true });
}

الخطوة 3: التوقيع بشهادة TUNTRUST

// src/ttn/signer.ts
import * as forge from "node-forge";
import * as fs from "fs";
import { create } from "xmlbuilder2";
 
interface SigningResult {
  signedXml: string;
  signatureValue: string;
  certificateBase64: string;
}
 
export function signTEIF(xmlContent: string): SigningResult {
  // تحميل شهادة PKCS#12
  const p12Buffer = fs.readFileSync(process.env.TUNTRUST_CERT_PATH!);
  const p12Der = forge.util.createBuffer(p12Buffer.toString("binary"));
  const p12Asn1 = forge.asn1.fromDer(p12Der);
  const p12 = forge.pkcs12.pkcs12FromAsn1(
    p12Asn1,
    process.env.TUNTRUST_CERT_PASSWORD!
  );
 
  // استخراج المفتاح الخاص والشهادة
  const bags = p12.getBags({
    bagType: forge.pki.oids.pkcs8ShroudedKeyBag,
  });
  const keyBag = bags[forge.pki.oids.pkcs8ShroudedKeyBag]?.[0];
  if (!keyBag?.key) throw new Error("المفتاح الخاص غير موجود في PKCS#12");
  const privateKey = keyBag.key;
 
  const certBags = p12.getBags({ bagType: forge.pki.oids.certBag });
  const certBag = certBags[forge.pki.oids.certBag]?.[0];
  if (!certBag?.cert) throw new Error("الشهادة غير موجودة في PKCS#12");
  const certificate = certBag.cert;
 
  // معيرة XML (C14N مبسّط لهذا المثال)
  const xmlBuffer = Buffer.from(xmlContent, "utf8");
 
  // حساب بصمة SHA-256 للوثيقة
  const md = forge.md.sha256.create();
  md.update(xmlBuffer.toString("binary"));
  const digestValue = Buffer.from(md.digest().bytes(), "binary").toString(
    "base64"
  );
 
  // توقيع البصمة بالمفتاح الخاص RSA
  const signatureMd = forge.md.sha256.create();
  signatureMd.update(xmlBuffer.toString("binary"));
  const signatureBytes = privateKey.sign(signatureMd);
  const signatureValue = Buffer.from(signatureBytes, "binary").toString(
    "base64"
  );
 
  // ترميز الشهادة بـ Base64
  const certDer = forge.asn1.toDer(forge.pki.certificateToAsn1(certificate));
  const certificateBase64 = Buffer.from(certDer.bytes(), "binary").toString(
    "base64"
  );
 
  // بناء قسم Signature XML وحقنه في الوثيقة
  const signatureXml = `
  <Signature Id="TEIF-SIG-001" xmlns="http://www.w3.org/2000/09/xmldsig#">
    <SignedInfo>
      <CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/>
      <SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
      <Reference URI="">
        <Transforms>
          <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
        </Transforms>
        <DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
        <DigestValue>${digestValue}</DigestValue>
      </Reference>
    </SignedInfo>
    <SignatureValue>${signatureValue}</SignatureValue>
    <KeyInfo>
      <X509Data>
        <X509Certificate>${certificateBase64}</X509Certificate>
      </X509Data>
    </KeyInfo>
  </Signature>`;
 
  // إدراج التوقيع قبل إغلاق <Invoice>
  const signedXml = xmlContent.replace(
    "</Invoice>",
    `${signatureXml}\n</Invoice>`
  );
 
  return { signedXml, signatureValue, certificateBase64 };
}

الخطوة 4: توليد رمز QR

// src/ttn/qrcode-generator.ts
import QRCode from "qrcode";
import crypto from "crypto";
 
export async function generateInvoiceQRCode(params: {
  invoiceId: string;
  supplierTaxId: string;
  issueDate: string;
  totalAmount: number;
  signatureValue: string;
}): Promise<string> {
  // توليد بصمة مختصرة لمعامل sig (أول 8 أحرف من بصمة SHA256)
  const sigHash = crypto
    .createHash("sha256")
    .update(params.signatureValue)
    .digest("hex")
    .substring(0, 8);
 
  // توليد معرّف TTN للفاتورة (بصمة رقم الفاتورة + المعرّف الجبائي)
  const iid = crypto
    .createHash("sha256")
    .update(`${params.invoiceId}:${params.supplierTaxId}`)
    .digest("hex")
    .substring(0, 12);
 
  // تنسيق التاريخ لرمز QR (YYYYMMDDHHMMSS)
  const dt = params.issueDate.replace(/-/g, "") + "000000";
 
  const verifyUrl = new URL("https://verify.elfatoora.tn/v");
  verifyUrl.searchParams.set("iid", iid);
  verifyUrl.searchParams.set("sid", params.supplierTaxId);
  verifyUrl.searchParams.set("dt", dt);
  verifyUrl.searchParams.set("amt", params.totalAmount.toFixed(3));
  verifyUrl.searchParams.set("sig", sigHash);
 
  // توليد رمز QR بـ Base64 (للدمج في XML أو PDF)
  const qrCodeDataUrl = await QRCode.toDataURL(verifyUrl.toString(), {
    errorCorrectionLevel: "M",
    type: "image/png",
    width: 200,
    margin: 1,
  });
 
  // إرجاع الجزء Base64 فقط (دون البادئة data:image/png;base64,)
  return qrCodeDataUrl.replace("data:image/png;base64,", "");
}

الخطوة 5: الإرسال إلى API TTN وإدارة الاستجابات

// src/ttn/client.ts
import axios, { AxiosError } from "axios";
import { getTTNAccessToken } from "./auth";
 
export interface TTNSubmitResponse {
  invoiceId: string;         // المعرّف الداخلي TTN
  status: "SUBMITTED" | "VALID" | "INVALID";
  validationErrors?: Array<{
    code: string;
    field: string;
    message: string;
  }>;
  timestamp: string;
  acknowledgementUrl?: string;
}
 
export class TTNApiError extends Error {
  constructor(
    public readonly code: string,
    public readonly details: string,
    public readonly httpStatus: number
  ) {
    super(`TTN API Error [${code}]: ${details}`);
  }
}
 
export async function submitInvoice(
  signedXml: string
): Promise<TTNSubmitResponse> {
  const token = await getTTNAccessToken();
 
  try {
    const response = await axios.post<TTNSubmitResponse>(
      `${process.env.TTN_API_BASE_URL}/api/v1/invoices`,
      signedXml,
      {
        headers: {
          Authorization: `Bearer ${token}`,
          "Content-Type": "application/xml",
          Accept: "application/json",
        },
        timeout: 30000, // 30 ثانية
      }
    );
 
    return response.data;
  } catch (error) {
    if (error instanceof AxiosError && error.response) {
      const { status, data } = error.response;
 
      // أخطاء التحقق TEIF (400)
      if (status === 400) {
        throw new TTNApiError(
          data.code || "VALIDATION_ERROR",
          JSON.stringify(data.errors || data.message),
          status
        );
      }
 
      // خطأ المصادقة (401)
      if (status === 401) {
        // إلغاء ذاكرة التخزين المؤقت للرمز وإعادة المحاولة
        throw new TTNApiError("AUTH_EXPIRED", "انتهت صلاحية الرمز، أعد المحاولة", 401);
      }
 
      // تجاوز الحصة (429)
      if (status === 429) {
        const retryAfter = error.response.headers["retry-after"] || "60";
        throw new TTNApiError(
          "RATE_LIMIT",
          `تجاوزت حصة API. أعد المحاولة خلال ${retryAfter} ثانية`,
          429
        );
      }
 
      throw new TTNApiError(
        data.code || "API_ERROR",
        data.message || "خطأ API غير معروف",
        status
      );
    }
 
    throw error;
  }
}
 
export async function getInvoiceStatus(
  ttnInvoiceId: string
): Promise<{ status: string; updatedAt: string }> {
  const token = await getTTNAccessToken();
 
  const response = await axios.get(
    `${process.env.TTN_API_BASE_URL}/api/v1/invoices/${ttnInvoiceId}`,
    { headers: { Authorization: `Bearer ${token}` } }
  );
 
  return response.data;
}

التنسيق: الخدمة الكاملة

// src/ttn/invoice-service.ts
import { generateTEIF, InvoiceData } from "./teif-generator";
import { signTEIF } from "./signer";
import { generateInvoiceQRCode } from "./qrcode-generator";
import { submitInvoice, TTNApiError } from "./client";
 
export interface InvoiceSubmissionResult {
  ttnInvoiceId: string;
  status: string;
  submittedAt: string;
  signatureValue: string;
  qrCodeBase64: string;
}
 
export async function createAndSubmitInvoice(
  invoiceData: InvoiceData
): Promise<InvoiceSubmissionResult> {
  console.log(`[TTN] توليد الفاتورة ${invoiceData.invoiceNumber}...`);
 
  // 1. توليد XML TEIF
  const xmlContent = generateTEIF(invoiceData);
 
  // 2. توقيع الوثيقة
  console.log("[TTN] توقيع وثيقة XML...");
  const { signedXml, signatureValue } = signTEIF(xmlContent);
 
  // 3. حساب المبلغ الإجمالي شامل TVA لرمز QR
  const totalTTC = invoiceData.lines.reduce((sum, line) => {
    const lineTotal = line.quantity * line.unitPrice;
    return sum + lineTotal + (lineTotal * line.vatRate) / 100;
  }, 0);
 
  // 4. توليد رمز QR
  console.log("[TTN] توليد رمز QR...");
  const qrCodeBase64 = await generateInvoiceQRCode({
    invoiceId: invoiceData.invoiceNumber,
    supplierTaxId: invoiceData.supplierTaxId,
    issueDate: invoiceData.issueDate,
    totalAmount: totalTTC,
    signatureValue,
  });
 
  // 5. الإرسال إلى API TTN مع إعادة المحاولة التلقائية
  let lastError: Error | null = null;
  for (let attempt = 1; attempt <= 3; attempt++) {
    try {
      console.log(`[TTN] الإرسال إلى API (المحاولة ${attempt}/3)...`);
      const response = await submitInvoice(signedXml);
 
      console.log(
        `[TTN] تم إرسال الفاتورة بنجاح. معرّف TTN: ${response.invoiceId}`
      );
 
      return {
        ttnInvoiceId: response.invoiceId,
        status: response.status,
        submittedAt: response.timestamp,
        signatureValue,
        qrCodeBase64,
      };
    } catch (error) {
      if (error instanceof TTNApiError) {
        // أخطاء غير قابلة للاسترداد — لا إعادة محاولة
        if (error.httpStatus === 400 || error.httpStatus === 401) {
          throw error;
        }
        // خطأ 429 أو 5xx — انتظر وأعد المحاولة
        lastError = error;
        const delay = attempt * 2000; // 2s, 4s, 6s
        console.warn(
          `[TTN] خطأ ${error.code}، إعادة المحاولة خلال ${delay}ms...`
        );
        await new Promise((resolve) => setTimeout(resolve, delay));
      } else {
        throw error;
      }
    }
  }
 
  throw lastError || new Error("فشل الإرسال بعد 3 محاولات");
}

الخيار 2: وسيط NGSign API

إن كنت لا تريد التعامل مع تشفير التوقيع مباشرةً، تقدم TTN خدمة وسيط تُسمى NGSign API. يتولى هذا الوسيط التوقيع الرقمي ويبسّط التكامل بشكل ملحوظ.

المزايا:

  • لا حاجة لإدارة محلية لشهادة PKCS#12
  • تحديث تلقائي عند تغيير معايير التوقيع
  • خدمة SaaS مُدارة من TTN

تدفق NGSign:

نظامك
    │ (XML TEIF غير موقَّع)
    ▼
NGSign API (TTN)
    │ (توقيع + توثيق زمني)
    ▼
API الفاتورة (TTN)
    │ (تحقق + أرشفة)
    ▼
استجابة لنظامك
// الإرسال عبر NGSign (دون إدارة محلية للتوقيع)
async function submitViaNGSign(xmlContent: string): Promise<string> {
  const token = await getTTNAccessToken();
 
  const response = await axios.post(
    `${process.env.TTN_API_BASE_URL}/api/v1/ngsign/submit`,
    {
      xmlContent: Buffer.from(xmlContent).toString("base64"),
      certificateAlias: process.env.TUNTRUST_CERT_ALIAS, // اسم مستعار مُعدَّ مسبقًا في NGSign
      signingMode: "SERVER", // تُوقّع TTN بالشهادة المخزّنة لديها
    },
    {
      headers: {
        Authorization: `Bearer ${token}`,
        "Content-Type": "application/json",
      },
    }
  );
 
  return response.data.invoiceId;
}

ملاحظة: يستلزم NGSign إيداع شهادتك TUNTRUST لدى TTN، مما يُثير مخاوف أمنية (المفتاح الخاص يخرج من سيطرتك). قيّم هذه المقايضة وفق سياسة الأمان لديك.


الخيار 3: التطوير المخصص الكامل

للمؤسسات الكبرى ذات الاحتياجات المحددة (كيانات متعددة، تكامل ERP معقد، أحجام مرتفعة جدًا)، قد تفرض بنية مخصصة نفسها.

بنية نموذجية:

ERP / نظام المصدر
    │ (أحداث إنشاء الفواتير)
    ▼
طابور الرسائل (Redis / RabbitMQ)
    │
    ▼
مجموعة العمال TTN (Node.js / microservice)
    ├── مُولِّد TEIF
    ├── وحدة التوقيع (HSM أو PKCS#12)
    ├── عميل API TTN
    └── مُدير الأخطاء / إعادة المحاولة
    │
    ▼
قاعدة البيانات (PostgreSQL)
    ├── حالات الفواتير
    ├── وصولات استلام TTN
    └── سجلات المراجعة

تتيح هذه البنية معالجة آلاف الفواتير يوميًا بأقصى قدر من الموثوقية والتتبع الكامل والاسترداد التلقائي عند الأخطاء.


الاختبار على sandbox TTN

قبل الانتقال إلى بيئة الإنتاج، يجب بالضرورة التحقق من تكاملك على بيئة الاختبار TTN.

إعداد بيئة الاختبار

// src/config.ts
export const TTN_CONFIG = {
  baseUrl:
    process.env.NODE_ENV === "production"
      ? "https://api.elfatoora.tn"
      : "https://api-sandbox.elfatoora.tn",
  timeoutMs: 30000,
  maxRetries: 3,
};

سيناريوهات الاختبار الإلزامية

تشترط TTN التحقق من السيناريوهات الآتية على الأقل في بيئة الاختبار قبل تفعيل الإنتاج:

// src/tests/ttn-sandbox.test.ts
import { createAndSubmitInvoice } from "../ttn/invoice-service";
 
const baseInvoiceData = {
  supplierTaxId: process.env.SUPPLIER_TAX_ID!,
  supplierName: process.env.SUPPLIER_NAME!,
  supplierAddress: "عنوان الاختبار",
  supplierCity: "Tunis",
  supplierPostalCode: "1000",
};
 
// اختبار 1: فاتورة قياسية مع TVA 19%
async function testStandardInvoice() {
  const result = await createAndSubmitInvoice({
    ...baseInvoiceData,
    invoiceNumber: `TEST-${Date.now()}`,
    issueDate: new Date().toISOString().split("T")[0],
    dueDate: new Date(Date.now() + 30 * 86400000).toISOString().split("T")[0],
    customerTaxId: "99999999A000TEST",
    customerName: "عميل اختبار Sandbox",
    customerAddress: "عنوان العميل الاختباري",
    customerCity: "Sfax",
    customerPostalCode: "3000",
    lines: [
      {
        id: 1,
        description: "خدمة اختبار",
        quantity: 1,
        unitCode: "C62",
        unitPrice: 100.0,
        vatRate: 19,
      },
    ],
  });
 
  console.assert(result.status === "VALID", "يجب أن تكون فاتورة الاختبار VALID");
  console.log("الاختبار 1 ناجح:", result.ttnInvoiceId);
}
 
// اختبار 2: إشعار دائن (avoir)
async function testCreditNote() {
  // مماثل لكن مع InvoiceTypeCode = "381"
  // ...
}
 
// اختبار 3: فاتورة متعددة البنود مع TVA مختلط
async function testMixedVATInvoice() {
  // بنود بـ TVA 19% و 7%
  // ...
}
 
// تشغيل جميع الاختبارات
(async () => {
  await testStandardInvoice();
  await testCreditNote();
  await testMixedVATInvoice();
  console.log("جميع اختبارات sandbox ناجحة!");
})();

قائمة التحقق للنشر في الإنتاج

قبل الانتقال إلى الإنتاج، تحقق من كل نقطة في هذه القائمة:

الأمان:

  • شهادة TUNTRUST مخزّنة في خزنة آمنة (AWS Secrets Manager، HashiCorp Vault أو ما يعادلها)
  • كلمة مرور الشهادة غير موجودة بشكل صريح في الكود المصدر أو السجلات
  • بيانات اعتماد API TTN في متغيرات البيئة، خارج مستودع Git
  • الوصول إلى خدمة الفوترة محمي بمصادقة داخلية

التقني:

  • جميع سيناريوهات اختبار sandbox اجتازت بحالة VALID
  • إدارة الأخطاء تشمل رموز 400 و401 و429 و500 و503
  • آلية إعادة المحاولة مع التراجع الأسي منفَّذة
  • السجلات تتضمن معرّف TTN لكل فاتورة مُرسَلة
  • قاعدة البيانات تحفظ الحالة ووصل الاستلام لكل فاتورة

القانوني والامتثال:

  • تم تفعيل الوصول إلى إنتاج TTN من قِبل TTN بعد التحقق من الاختبارات
  • تم إيداع تصريح الانضمام لدى DGI
  • استراتيجية الأرشفة (5 سنوات كحد أدنى) جاهزة

التشغيلي:

  • التنبيهات مُعدَّة لأخطاء الإرسال
  • لوحة متابعة الفواتير متاحة لقسم المحاسبة
  • إجراء التسوية في حالة الخطأ موثَّق

التنقل في السلسلة

  • الحلقة السابقة: الدمج في برنامج المحاسبة الخاص بك (قريبًا)
  • الحلقة التالية: الأرشفة والالتزامات القانونية بعد الفوترة (قريبًا)

للعودة إلى الأساسيات التقنية، راجع الحلقة الخامسة: تنسيق TEIF والمواصفات التقنية.


هل تودّ مؤسستك دمج الفوترة الإلكترونية TTN دون استنزاف مواردك التطويرية الداخلية؟ تقدم noqta.tn حلًا متكاملًا جاهزًا — من تحليل نظامك الحالي إلى النشر في الإنتاج، مرورًا بتدريب فرقك. لقد رافقنا عشرات المؤسسات التونسية في هذا التحول.

تواصل مع فريقنا التقني للحصول على تشخيص مجاني لنظامك وتقدير مشروع التكامل.


هل تريد قراءة المزيد من الدروس التعليمية؟ تحقق من أحدث درس تعليمي لدينا على أفضل ممارسات البرمجة التوليدية: تقنيات متقدمة للتطوير بمساعدة الذكاء الاصطناعي.

ناقش مشروعك معنا

نحن هنا للمساعدة في احتياجات تطوير الويب الخاصة بك. حدد موعدًا لمناقشة مشروعك وكيف يمكننا مساعدتك.

دعنا نجد أفضل الحلول لاحتياجاتك.

مقالات ذات صلة