ZATCA FLUTTER

zatca_flutter is a Flutter plugin that provides seamless integration with the ZATCA (Zakat, Tax, and Customs Authority) e-invoicing system. It supports Windows and Linux environments, allowing developers to generate compliant electronic invoices efficiently.

The only flutter plugin that fully and perfectly implements with Zatca/Fatoora E-Invoicing Phase 1 & 2 requirements. It is the complete solution for creating a flutter e-invoicing desktop app (Windows & Linux) for use in Saudi Arabia.

Zatca Flutter Package Banner

Current Status

pub package pub points likes

Features

  • Setting up Business Info.
  • Generating and saving CSR related files.
  • Generating and saving XML files for invoices, debit notes, and credit notes.
  • Generating and Displaying QR codes for invoice/note.
  • Validating XML invoice/note.
  • Signing and saving the signed invoices and notes.
  • Generating Invoice/Note Hash Value (Typically used as PIH for next invoice)
  • Generating ApiRequest for Compliance Check, Reporting and Clearance
  • Generating and saving Compliance CSID and Production CSID.
  • Checking invoice/note/system compliance.
  • Clearing Invoice/Note.
  • Reporting Invoice/Note.
  • Converting and Saving Keys/Certs.
  • Invoice, debit note, and credit note reporting and clearance.

Installation

Add zatca_flutter to your pubspec.yaml:

Run the command below

flutter pub add zatca_flutter

Usage

Initialization

You only have to The three modes acceptable are sandbox, simulation, and production. Sandbox uses the developer-portal zatca server endpoint.

void main() {
    WidgetsFlutterBinding.ensureInitialized();

    // This all that is required. Switch mode as per needed.
    ZatcaFlutter.init(mode: Mode.sandbox);
    
    runApp(const App());
}

Business Setup

// Just assign MyBusinessInfo model to the LocalStore `myBusinessInfo`. That automatically saves it for future reference.
LocalStore.instance.myBusinessInfo = MyBusinessInfo(
    name: _nameContr.text,
    address: _addressContr.text,
    taxId: _taxIdContr.text,
    buildingNumber: _buildingNumberContr.text,
    citySubdivision: _citySubdivisionContr.text,
    city: _cityContr.text,
    postalZone: _postalZoneContr.text,
    countryCode: _countryCodeContr.text,
    schemeID: _schemeIDContr.text,
    companyID: _companyIdContr.text,
    businessID: _businessIdContr.text
);

Actually, by just creating an instance of MyBusinessInfo, the business info is automatically saved and assigned to the myBusinessInfo of the LocalStore. If you are in doubt, you can still explicitly save the myBusinessInfo by calling its save() method which is actually called behind the scene when you just create an instance.

CSR Config File Generation

CsrConfig(
    commonName: 'Your Common Name',
    serialNumber: 'SerialN',
    organizationIdentifier: '...',
    organizationUnitName:'Organization unit name',
    organizationName: 'Organization Name',
    countryName: 'Country name (SA)',
    invoiceType: 'Typically 1100',
    locationAddress: 'Business Address',
    industryBusinessCategory: 'Business Category');

Once an instance of CsrConfig is created, the csrConfig.properties file will automatically be saved.

After creating an instance of CsrConfig file, you can await the value of the filePath where the file is created if you need to by just doing the following:

CsrConfig csrConfig = CsrConfig(
    commonName: 'Your Common Name',
    serialNumber: 'SerialN',
    organizationIdentifier: '...',
    organizationUnitName:'Organization unit name',
    organizationName: 'Organization Name',
    countryName: 'Country name (SA)',
    invoiceType: 'Typically 1100',
    locationAddress: 'Business Address',
    industryBusinessCategory: 'Business Category');

// Await the file path as shown below
String csrFilePath = await csrConfig.filePath;

Also, note that you could also load an already created file from the filesystem after it has been created.

CsrConfig csrConfig = await CsrConfig.load();

Generate CSR file

The simplest way is to call FatooraService.generateCsr() method with the only required argument csrConfigFile passed in. The outputCsrFile and the privateKeyFile are optional.

FatooraServiceCsrResponse csrResponse = FatooraService.generateCsr(csrConfigFile: csrConfigFile);

Note: csrConfigFile is the file name from previous step above. See the example app for illustration or read the documentation.

The FatooraServiceCsrResponse has the following properties csrOutputFileName and keyOutputFileName that you can make reference to if you want to read the csr value and private key value respectively.

Reading the Generated CSR Value and Private Key Value

Just use the utility function getFileContentAsString(fileName);

Example of reading the csr value and key value from above.

String csr = getFileContentAsString(response.csrOutputFileName);
String key = getFileContentAsString(response.keyOutputFileName);

Requesting CCSID from Zatca

CCSIDRequestProp prop = CCSIDRequestProp(csr: csr, otp: otp);
ComplianceCSIDResponse ccsidResponse = await ZatcaFlutter.request.requestComplianceCSID(request: prop);

See the documentation for properties of the ComplianceCSIDResponse

Requesting PCSID from Zatca

For you to request PCSID you must have first done the CCSID request. Then do the following:

ProductionCSIDResponse response = await ZatcaFlutter.request.requestProductionCSIDOnboarding();

See the documentation for properties of the ProductionCSIDResponse

Save the Returned Token and Key

After requesting for CCSID, if the request was successful, a binary token and a key will be returned. You can save it by doing the following:

request.convertTokenAndKeyToPemAndSaveToSDKForSigning(certAndKey: CertAndKey(cert: ccsidResponse.successData, key: key))

After the above steps have been completed, you can then follow the following steps for regular processes.

Generating An Invoice/Note

There are two types of Invoices. These are SimplifiedInvoice and StandardInvoice. There are two types of Notes. These are Credit Note and Debit Note. Each of these types of notes could either be Simplified or Standard. So that makes it a total of 6 major types of xml files you can generate with zatca_flutter package. Example:

// Create the invoice object
SimplifiedInvoice simplifiedInvoice = SimplifiedInvoice(
    icv: 1,
    pih:'', // Pass in an empty string like this if this is your first invoice. Otherwise, pass in the actual PIH (Previous Invoice Hash)
    id: "INV-001",
    uuid: generateUuid(),
    issueDate: DateTime.now(),
    issueTime: DateTime.now(),
    currency: 'SAR',
    customer: IndividualParty(
        name: "Elizabeth",
        taxId: "310298993344553",
        address: "address 3az",
    ),
    lines: [
        InvoiceLine(
            quantity: "1",
            price: "64",
            total: "64",
            tax: TaxDetails(
                amount: "9.60", // 64 * 15% = 9.60
                percent: "15",
                currency: 'SAR',
                taxableAmount:'64',
                code: TaxCategoryCode.standard, // Ensure taxable amount matches total
            ),
            name: 'Garri',
        )
    ],
    tax: TaxDetails(
        amount: "9.60", // Sum of line-level tax amounts
        percent: "15",
        currency: 'SAR',
        taxableAmount: '64', code: TaxCategoryCode.standard, // Total taxable amount
    ),
    monetaryTotal: LegalMonetaryTotal(
        lineExtensionAmount: "64", // Sum of line totals
        taxExclusiveAmount: "64", // Same as lineExtensionAmount
        taxInclusiveAmount: "73.60", // taxExclusiveAmount + total VAT
        payableAmount: "73.60", // Final payable amount
        allowanceTotalAmount: 0.0,
        prepaidAmount: 0.0
    ),
);

simplifiedInvoice.generateAndSaveXml('simple_invoice.xml');   // Generate and save the invoice xml data from the invoice object

Signing An Invoice/Note

Signing an invoice requires using the necessary certificate. But you don't need to worry about that since the plugin handles that for you. However, you need to specify if the signing you are doing is for checks or not by passing in isForComplianceCheck:true if you want to use it for local (fatoora) validation check or server (zatca) compliance check. If you want to use the signed document for reporting (as required for Simplified Invoice/Note reporting) then you might omit the isForComplianceCheck or explicitly indicate it's for reporting by passing in isForComplianceCheck:false to the FatooraService.signInvoice() method. Example:

// Sign the invoice if it's Simplified (B2C)
FatooraServiceResponse signRes = await FatooraService.signInvoice(invoiceFileName: 'simple_invoice.xml', isForComplianceCheck: true);
// The signed invoice for the above will be simple_invoice_signed.xml

Note: if you do not specify the outputSignedInvoiceFileName, the signed invoice will have the name of the original invoice above with _signed.xml added as the last part.

Generating Invoice RequestAPI FatooraService.generateInvoiceRequestAPI

Invoice/Note request API is the JSON data format that consist of uuid, invoiceHash and invoice data and is required for invoice/note compliance check, reporting, and clearance. If you want to generate the request API for clearance, you should pass in forClearance:true to the FatooraService.generateInvoiceRequestAPI() method. Otherwise, the plugin assumes you are generating the request API for reporting.

// Generate the Request API for compliance check, reporting or clearance
FatooraInvoiceRequestApiResponse invoiceApi =
    await FatooraService.generateInvoiceRequestAPI(
        invoiceFileName: 'simple_invoice_signed.xml');

Example of Reporting An Invoice B2C

// Create the invoice object
SimplifiedInvoice simplifiedInvoice = SimplifiedInvoice(
    icv: 1,
    pih:
        '', // Pass in an empty string like this if this is your first invoice. Otherwise, pass in the actual PIH (Previous Invoice Hash)
    id: "INV-001",
    uuid: generateUuid(),
    issueDate: DateTime.now(),
    issueTime: DateTime.now(),
    currency: 'SAR',
    customer: IndividualParty(
    name: "Elizabeth",
    taxId: "310298993344553",
    address: "address 3az",
    ),
    lines: [
    InvoiceLine(
        quantity: "1",
        price: "64",
        total: "64",
        tax: TaxDetails(
        amount: "9.60", // 64 * 15% = 9.60
        percent: "15",
        currency: 'SAR',
        taxableAmount: '64',
        code: TaxCategoryCode
            .standard, // Ensure taxable amount matches total
        ),
        name: 'Garri',
    )
    ],
    tax: TaxDetails(
    amount: "9.60", // Sum of line-level tax amounts
    percent: "15",
    currency: 'SAR',
    taxableAmount: '64',
    code: TaxCategoryCode
        .standard, // Total taxable amount
    ),
    monetaryTotal: LegalMonetaryTotal(
        lineExtensionAmount: "64", // Sum of line totals
        taxExclusiveAmount:
            "64", // Same as lineExtensionAmount
        taxInclusiveAmount:
            "73.60", // taxExclusiveAmount + total VAT
        payableAmount: "73.60", // Final payable amount
        allowanceTotalAmount: 0.0,
        prepaidAmount: 0.0),
);

// XML creation
simplifiedInvoice.generateAndSaveXml(
    'simple_invoice.xml'); // Generate and save the invoice xml data from the invoice object

// Sign the invoice if it's Simplified (B2C)
FatooraServiceResponse signRes =
    await FatooraService.signInvoice(
        invoiceFileName: 'simple_invoice.xml',
        isForComplianceCheck: true);

// Get the XML of the signed invoice
xml = await getFileContentAsString(
    'simple_invoice_signed.xml');

// Get the QR Code from the signed invoice
FatooraQrCodeResponse qrCodeResponse =
    await FatooraService.generateInvoiceQRCode(
        'simple_invoice_signed.xml');

// Get the qrCode String like this
String base64EncodedQrCode = qrCodeResponse.qrCode;


debugPrint(
    "SIGNATURE (${signRes.status.name.toUpperCase()}): ${signRes.infos?.map((d) => d.message)}");

// Validate signed invoice locally before doing compliance check, reporting or clearance
FatooraServiceResponse validationRes =
    await FatooraService.validateInvoice(
        invoiceFileName: 'simple_invoice_signed.xml',
        ignoreWarningForResponseStatus: true);

debugPrint(
    "VALIDATION (${validationRes.status.name.toUpperCase()}): SUCCESS (${validationRes.infos?.length}); FAILURE (${validationRes.errors?.length}) WARNING (${validationRes.warnings?.length}) ${validationRes.infos?.map((d) => d.message)}");

// Generate the Request API for compliance check, reporting or clearance
FatooraInvoiceRequestApiResponse invoiceApi =
    await FatooraService.generateInvoiceRequestAPI(
        invoiceFileName: 'simple_invoice_signed.xml');

// Check compliance on zatca server.
ComplianceInvoiceCheckResponse? res = await Controller
    .instance.request
    .requestComplianceCheck(
        prop: invoiceApi.invoiceRequest!);

if (res?.status != null) {
    setState(() {
    responseStatus =
        "${res?.clearanceStatus ?? res?.reportingStatus ?? res?.status.name.toUpperCase()}\n\n${res?.validationResults?.toJson()}";
    });
}

Example of Executing Invoice Clearance

StandardInvoice standardInvoice = StandardInvoice(
    icv: 1,
    pih: '',
    id: "INV-001",
    uuid: generateUuid(),
    issueDate: DateTime.now(),
    issueTime: DateTime.now(),
    currency: 'SAR',
    customer: BusinessParty(
        businessID: '1010010000',
        address: "RRRD2929",
        name: "Info Tech Supply LTD",
        taxId: "399999999955553",
        buildingNumber: "3233",
        citySubdivision: "Al-Murabba",
        city: "Riyadh",
        postalZone: "23333",
        countryCode: "SA",
        schemeID: 'CRN'),
    lines: [
        InvoiceLine(
        quantity: "1",
        price: "64",
        total: "64",
        tax: TaxDetails(
            amount: "9.60", // 64 * 15% = 9.60
            percent: "15",
            currency: 'SAR',
            taxableAmount: '64',
            code: TaxCategoryCode
                .standard, // Ensure taxable amount matches total
        ),
        name: 'Garri',
        )
    ],
    tax: TaxDetails(
        amount: "9.60", // Sum of line-level tax amounts
        percent: "15",
        currency: 'SAR',
        taxableAmount: '64',
        code:
            TaxCategoryCode.standard, // Total taxable amount
    ),
    monetaryTotal: LegalMonetaryTotal(
        lineExtensionAmount: "64", // Sum of line totals
        taxExclusiveAmount:
            "64", // Same as lineExtensionAmount
        taxInclusiveAmount:
            "73.60", // taxExclusiveAmount + total VAT
        payableAmount: "73.60", // Final payable amount
        allowanceTotalAmount: 0.0,
        prepaidAmount: 0.0),
    delivery: Delivery(
        actualDate: DateTime.now(),
        latestDate: DateTime.now()),
);

// Create the XML
standardInvoice
    .generateAndSaveXml('standard_invoice.xml');

// Request API generation
FatooraInvoiceRequestApiResponse invoiceApi =
    await FatooraService.generateInvoiceRequestAPI(
        invoiceFileName: 'standard_invoice.xml',
        forClearance: true);

debugPrint(
    "INVOICE API: ${invoiceApi.invoiceRequest?.toMap()}");
ZatcaFlutter zatca = ZatcaFlutter.instance;
InvoiceClearanceResponse? res = await zatca.request
    .requestClearance(invoiceApi.invoiceRequest!);

debugPrint(
    "RESULT FROM CLEARANCE ${res?.statusCode} ${res?.clearanceStatus} ${res?.validationResults?.toJson()}");

debugPrint(
    "CLEARED INVOICE: \n${res?.clearedInvoice}\n\nCLEARANCE DATA: \n${res?.fileName}");

String? qrCode = await ClearedInvoiceService.getQrCode(res.fileName!);  // To get the qrCode of the cleared invoice


String? invoiceHash = await ClearedInvoiceService.getInvoiceHash(res.fileName!);  // To get the qrCode of the cleared invoice

Displaying the QR CODE

QrCodeImage(base64EncodedQrCode: qrCode);

See the documentation or example app for more usage.

Sponsorship

Please support us to keep maintaining this package if you find it useful and want to help us.

Sponsor Us on GitHub

Thanks

Contributing

If you are interested in contributing to this package reach-out to me via GitHub.

Connect With the Author

Engineer Isaiah Pius E. I am pleased to have you use this package. You can reach-out to me via my GitHub, Email and Linked. See my GitHub page for relevant connection links. Thanks once again.