Technical Documentation

This document describes the backend architecture of Gestion...

Technical Documentation ΒΆ

This document describes the backend architecture of Gestion, a Nextcloud application for billing and invoice management. It covers the directory layout, the database schema, all service classes and their methods, routing, controllers, and external dependencies.

1. Architecture Overview ΒΆ

Gestion follows the standard Nextcloud MVC architecture. PHP controllers handle HTTP routing, services encapsulate all business logic, and a single database class (Bdd) centralises all SQL access. The frontend is a Vue/JS SPA compiled into the js/ directory.

Directory structure

PathRole
lib/Controller/HTTP layer β€” maps routes to service calls. No business logic lives here.
lib/Service/Business logic layer β€” each service handles one functional domain (PDF generation, company management, data access, etc.).
lib/Db/Bdd.phpSole database abstraction. All SQL queries are written here using whitelist-protected prepared statements.
lib/Migration/Database schema migrations β€” one file per version bump, run by Nextcloud automatically on update.
lib/AppInfo/Application.phpApp bootstrap β€” registers services in the dependency injection container.
lib/Settings/ & lib/Sections/Nextcloud admin panel integration β€” registers the Gestion settings page.
templates/PHP/HTML templates rendered server-side for page views.
src/Frontend source (Vue / Less). Compiled output goes into js/ and dist/.
appinfo/routes.phpDeclares all HTTP routes and maps them to controller actions.

Database tables

All tables are prefixed with oc_gestion_. They are scoped per company via an id_configuration foreign key, allowing one Nextcloud instance to host multiple independent companies.

TableContent
configurationCompany settings: name, address, VAT number, IBAN, invoice prefix, logo paths, shared access list.
conf_shareMany-to-many: which Nextcloud users share access to a given configuration.
clientCustomer records linked to a configuration.
devisQuotes, linked to a client and a configuration.
produitProduct / service catalogue, scoped per configuration.
produit_devisJoin table between quotes and products (quantity, unit price override, display order).
factureInvoices, each linked to exactly one quote.

Request flow

# Full request path
Browser / JS frontend
  β†’ appinfo/routes.php  (URL matching)
  β†’ Controller          (thin wrapper β€” extracts params, returns response)
  β†’ Service             (business logic β€” validation, data assembly)
  β†’ Bdd                 (SQL β€” prepared statements)
  β†’ Nextcloud IDBConnection

Note

Controllers return either a DataResponse (JSON) or a TemplateResponse (HTML). The Bdd class always returns JSON-encoded strings from SELECT queries; services decode them as needed.

2. Routing (appinfo/routes.php) ΒΆ

Routes are declared as an associative array consumed by the Nextcloud framework. Each entry maps a verb + URL to a Controller#action pair.

VerbURLControllerAction
GET/ViewControllerRenders the main index page
GET/devisViewControllerRenders the quotes list page
GET/factureViewControllerRenders the invoices list page
GET/produitViewControllerRenders the products page
GET/configConfigurationControllerRenders the company config page
GET/isconfigConfigurationControllerReturns whether the config is set up
GET/statistiqueViewControllerRenders the statistics page
GET/backupAdminControllerExports a full CSV backup to cloud storage
PROPFIND/getClientsClientControllerReturns all clients as JSON
PROPFIND/getDevisDevisControllerReturns all quotes as JSON
PROPFIND/getFacturesFactureControllerReturns all invoices as JSON
PROPFIND/getProduitsProduitControllerReturns all products as JSON
PROPFIND/getConfigurationConfigurationControllerReturns the company configuration
PROPFIND/getStatsStatsControllerReturns entity counts
PROPFIND/getAnnualTurnoverPerMonthNoVatStatsControllerMonthly turnover data (excl. VAT)
POST/client/insertClientControllerCreates a new client with placeholder values
POST/devis/insertDevisControllerCreates a new quote
POST/facture/insertFactureControllerCreates a new invoice
POST/produit/insertProduitControllerCreates a new product
POST/updateCrudControllerGeneric single-field update
POST/dropCrudControllerReorders a product line in a quote (up/down)
POST/sendPDFPdfControllerEmails a PDF to recipients
POST/generatePDFPdfControllerGenerates and saves a PDF to cloud storage
POST/generateFacturXPdfControllerGenerates a Factur-X PDF+XML (EN16931)
POST/generateFacturXmlPdfControllerGenerates only the Factur-X XML file
POST/sendFacturXToIopolePdfControllerSubmits a Factur-X PDF to the Iopole platform
PUT/duplicateCrudControllerDeep-duplicates a record
PUT/createCompanyCompanyControllerCreates a new company
DELETE/deleteCrudControllerDeletes a record
DELETE/deleteCompanyCompanyControllerDeletes current company and all its data

3. Services (lib/Service/) ΒΆ

Services are the core of the application. Controllers are intentionally thin: they receive the HTTP request, call a single service method, and return its result. All validation, data assembly, and computation happen in services.

3.1 DataService

Central data-access facade. Wraps every Bdd read/write call and injects the current company ID (from the session) automatically. Controllers and other services that need data should call DataService, never Bdd directly.

MethodPurposeParametersReturns
getClients()Returns all clients belonging to the active company.β€”JSON string
getClient($id)Returns a single client by primary key.$id: intJSON string
getClientbyiddevis($id)Returns the client attached to a given quote.$id: int (quote id)JSON string
getConfiguration()Returns the company configuration row.β€”JSON string
getDevis()Returns all quotes with joined client name and company.β€”JSON string
getFactures()Returns all invoices with joined quote & client info.β€”JSON string
getOneDevis($id)Returns a single quote with full client join.$id: intJSON string
getOneFacture($id)Returns a single invoice with full join chain.$id: intJSON string
getProduits()Returns all catalogue products for the company.β€”JSON string
getProduitsById($devisId)Returns product lines for a specific quote.$devisId: intJSON string
insertClient()Inserts a new client with placeholder values.β€”bool
insertDevis()Inserts a new quote with placeholder values.β€”bool
insertFacture()Inserts a new invoice; auto-computes the number from the configured prefix.β€”int (new id)
insertProduit()Inserts a new product; reads the default VAT rate from config.β€”bool
insertProduitDevis($id)Attaches the first available product to a quote line.$id: int (quote id)int
update($table,$col,$data,$id)Updates a single field. Delegates to Bdd whitelist check.table, column, value, idbool
updateConfiguration(...)Updates a configuration field (no company scope check).table, column, value, idbool
duplicate($table,$id)Deep-duplicates a record; also clones product lines for quotes.table, idDataResponse
drop($id,$value)Moves a product line up or down in a quote.id, 'up'|'down'void
delete($table,$id)Deletes a record after whitelist validation.table, idbool
getStats()Returns a JSON object with entity counts: clients, quotes, invoices, products.β€”JSON string
getAnnualTurnoverPerMonthNoVat()Returns monthly turnover grouped by year/month (excl. VAT).β€”array
getServerFromMail()Reads Nextcloud system config to return the configured sender email address.β€”DataResponse

3.2 CompanyService

Manages multi-company support and shared access. A Nextcloud user can own several companies and also be invited to access companies owned by others. The active company ID is stored in the PHP session.

MethodPurposeParametersReturns
getCurrentCompany()Returns the company ID stored in the current session.β€”int|null
setCurrentCompany($id)Writes the chosen company ID to the session.$id: intvoid
getCompaniesList()Returns all companies the current user owns or has shared access to. Auto-selects the first one if the session is empty.β€”array
createCompany()Creates a new company record for the current user with default placeholder values.β€”void
deleteCompany()Deletes the active company and all its associated data (cascade). Only the owner can delete.β€”DataResponse
isConfig()Checks whether the company configuration is considered complete (uses an internal changelog counter to show the setup prompt once).β€”bool
ensureConfigExists()Verifies that a config record exists; clears the session company if not.β€”void
getShareUsers()Returns Nextcloud user objects for all users who have shared access to the active company.β€”array
addShareUser($email)Grants shared access by Nextcloud email. Only the owner can call this. Searches all users for the matching email.$email: stringDataResponse
delShareUser($uid)Revokes shared access for a given Nextcloud UID. Owner-only.$uid: stringDataResponse

3.3 PdfService

Handles all PDF generation and delivery. Uses the mPDF library for HTML-to-PDF rendering and GestionFacturXWriter for Factur-X metadata embedding. Also manages email delivery (legacy path) and delegates electronic invoice submission to IopoleService.

MethodPurposeParametersReturns
generatePDF($html,$name,$folder)Renders HTML as an A4 PDF via mPDF, saves it to the user's Nextcloud folder, and returns the binary as a download response.html, name, folderDataDownloadResponse
generateFacturX($html,$name,$folder,$id)Renders the PDF, builds the EN16931 XML via buildFacturXml(), embeds the XML into the PDF as XMP metadata using GestionFacturXWriter, then saves and returns the result.html, name, folder, factureIdDataDownloadResponse
generateFacturXml($id,$name,$folder)Builds only the EN16931 XML (no PDF rendering) and saves it as a .xml file.factureId, name, folderDataDownloadResponse
sendFacturXToIopole($html,$name,$id)Builds the Factur-X PDF and sends it to the Iopole platform via IopoleService. Returns the Iopole invoice ID on success.html, name, factureIdDataResponse
sendPDF($content,$name,$subject,$body,$to,$cc)Decodes a base64 PDF and sends it by email using Nextcloud IMailer. Returns 200 or 500 with a hint about SMTP config.content (base64), name, subject, body, to (JSON), cc (JSON)DataResponse
savePDF($content,$folder,$name)Delegates base64-encoded PDF saving to FileService.content (base64), folder, namevoid

Private helpers

  • renderPdf($html) β€” instantiates mPDF with A4 margins, injects style.css, returns raw PDF bytes.
  • buildFacturXml($factureId) β€” fetches invoice + config + product lines, computes VAT totals per rate, assembles a complete EN16931-compliant XML document.
  • buildFacturXPdfContent($html,$id) β€” combines renderPdf + buildFacturXml + GestionFacturXWriter into a single binary string.
  • formatAmount($amount) β€” rounds to 2 decimal places with a dot separator for XML output.

3.4 FacturXService

Provides a more abstract, object-oriented path to Factur-X generation. Exposes higher-level methods intended for reuse or external callers. The production path for invoices is currently handled by PdfService.

MethodPurposeParametersReturns
buildXml($invoice,$company,$customer,$products)Builds a complete EN16931 CrossIndustryInvoice XML from structured objects. Calls calculateTotals(), buildLineItems(), buildTaxes(), then assembles the full document.invoice, company, customer, products (objects)string (XML)
generateFacturXPdf($pdfContent,$xmlContent)Embeds an existing XML string into an existing PDF binary using GestionFacturXWriter. Returns the merged PDF bytes.pdfContent, xmlContent: stringstring (PDF bytes)

Private helpers

  • calculateTotals($products) β€” iterates product lines, groups VAT amounts by rate, returns totalHT / totalVAT / totalTTC and a vatLines breakdown.
  • buildLineItems($products) β€” generates one <ram:IncludedSupplyChainTradeLineItem> XML block per product.
  • buildTaxes($vatLines) β€” generates one <ram:ApplicableTradeTax> block per distinct VAT rate.
  • buildVatNumber($vatNumber) β€” ensures a French VAT number starts with FR; prepends FR00 + SIREN digits otherwise.
  • buildDocument(...) β€” assembles all sections into the final rsm:CrossIndustryInvoice XML envelope.

3.5 IopoleService

Handles authentication and PDF upload to the Iopole electronic invoicing platform (French e-invoicing intermediary). All connection settings are read from Nextcloud app config β€” nothing is hard-coded.

Required app config keys

iopole_client_id, iopole_client_secret, iopole_customer_id, iopole_base_url, iopole_auth_url β€” all must be set in the Nextcloud admin panel before use.

MethodPurposeParametersReturns
sendInvoice($pdfContent,$filename)Authenticates via OAuth2 client_credentials, writes the PDF to a temp file, POSTs it as multipart/form-data to /v1/invoice, validates the response, and returns the parsed payload including the Iopole invoice ID.pdfContent: string, filename: stringarray (payload)

Private helpers

  • requestAccessToken($authUrl,$clientId,$clientSecret) β€” performs the OAuth2 token exchange and returns the bearer token string.
  • request($url,$headers,$body) β€” thin cURL wrapper; returns ['status' => int, 'body' => string].
  • getRequiredAppValue($key) β€” reads a key from Nextcloud app config and throws RuntimeException if blank.
  • assertValidUrl($url,$key) β€” validates URL format with filter_var and throws if invalid.

3.6 MailService

Dedicated service for sending a PDF as an email attachment. Uses the Nextcloud IMailer abstraction so the underlying SMTP transport is configured at the Nextcloud system level.

MethodPurposeParametersReturns
sendPdf($content,$name,$subject,$body,$to,$cc)Decodes a base64 PDF, constructs a Nextcloud mail message with HTML body and PDF attachment, handles optional CC recipients from a JSON-encoded array, and sends. Returns a status array.content (base64), name, subject, body (HTML), to (JSON array), cc (JSON array)array ['status' => ...]

3.7 FileService

Low-level file access service for the current user's Nextcloud storage. Used by PdfService and TemplateService to read logos and save generated documents.

MethodPurposeParametersReturns
getLogo($name)Reads a file from the hidden .gestion/ folder and returns it as base64. Returns the string 'nothing' if the file does not exist.$name: stringstring (base64 or 'nothing')
savePDF($content,$folder,$name)Decodes a base64 string and saves the binary to the given Nextcloud path via saveContent().content (base64), folder, namevoid
saveContent($content,$folder,$name)Creates the target folder (silently ignores "already exists"), creates the file node, and writes the raw content.content (raw), folder, namevoid

3.8 StorageService

A more complete alternative to FileService with explicit error handling, existence checks, and deletion support. Provides a clean interface over the Nextcloud IRootFolder API.

MethodPurposeParametersReturns
getStorage()Returns the Nextcloud Folder node for the current user, or null if unavailable.β€”?Folder
ensureFolder($folder)Creates a folder at the given path; silently ignores if it already exists.$folder: stringvoid
saveFile($folder,$filename,$content)Creates the folder, creates or updates the file node, writes raw content. Returns true on success.folder, filename, content (raw)bool
savePdf($base64,$folder,$filename)Decodes base64 then delegates to saveFile().base64 content, folder, filenamebool
getFile($path)Returns raw file content for the given path, or null on error.$path: string?string
getLogo($filename)Reads from .gestion/ and returns base64-encoded content, or 'nothing'.$filename: stringstring
exists($path)Checks whether a node exists at the given path.$path: stringbool
delete($path)Retrieves and deletes a file node. Returns true on success.$path: stringbool

3.9 TemplateService

Assembles the data required for each server-rendered page and returns a Nextcloud TemplateResponse. Centralises shared base parameters (user, navigation URLs, company list) so controllers don't duplicate that logic.

MethodPurposeParametersReturns
page($template)Returns a TemplateResponse for any simple page (index, produit, statistique, etc.) with base parameters.$template: stringTemplateResponse
legalnotice($page)Returns the legal notice template with the specific sub-page identifier.$page: stringTemplateResponse
config()Returns the configuration template after calling ensureConfigExists(). Injects the list of shared users and attaches a permissive ContentSecurityPolicy.β€”TemplateResponse
devisshow($id)Fetches the quote, its product lines, and all three logo variants (main, header, footer) as base64, then renders the devisshow template.$id: int (quote id)TemplateResponse
factureshow($id)Fetches the invoice data and all three logo variants, then renders the factureshow template.$id: int (invoice id)TemplateResponse

Note

The private baseParams() method builds the common data payload: user path, navigation URLs, companies list, and current company ID. It is injected into every TemplateResponse.

3.10 NavigationService

Single-purpose service that generates absolute URLs for all navigation links using the Nextcloud IURLGenerator. Used by TemplateService to inject URLs into every page.

MethodPurposeReturns
getNavigationLink()Returns an associative array of named absolute URLs for: index, devis, facture, produit, config, isConfig, statistique, legalnotice, france.array

4. Database Layer (lib/Db/Bdd.php) ΒΆ

Bdd is the sole class that issues SQL. It uses Nextcloud's IDBConnection (PDO-based) and protects against SQL injection through two mechanisms: prepared statements with positional placeholders for all values, and whitelist arrays for table and column names.

Security model

  • $whiteTable: [client, devis, produit_devis, facture, produit, configuration] β€” only these table names are accepted in dynamic queries.
  • $whiteColumn: a fixed list of ~40 column names β€” only these are accepted as SET targets in UPDATE calls.
  • All user-supplied values are passed as prepared statement parameters, never interpolated into SQL strings.
  • Data is sanitised with strip_tags() (retaining <br>) and html_entity_decode() before writes.

Key query methods

  • getDevis() / getFactures() β€” JOINs devis/facture with client to return a denormalized list in one query.
  • getListProduit($devisId) β€” JOINs produit, produit_devis, and devis; orders by the produit_devis.order column.
  • gestion_update($table,$column,$data,$id,$config) β€” generic single-field updater after whitelist validation.
  • gestion_duplicate($table,$id) β€” dynamic column-list INSERT by SELECTing the source row; handles invoice number recomputation and product line cloning for quotes.
  • gestion_drop($id,$value) β€” swaps the order values of two adjacent product lines to move one up or down.
  • deleteCompany($id) β€” cascades deletion across all 7 tables.
  • backup() β€” dumps all six tables as a flat array for CSV export.
  • getAnnualTurnoverPerMonthNoVat() β€” aggregates (price Γ— quantity) grouped by YEAR + MONTH of date_paiement.

Internal execution helpers

  • execSQL($sql,$conditions) β€” prepares, executes, fetches all rows, returns JSON-encoded string.
  • execSQLNoData($sql,$conditions) β€” prepares and executes; no return value (INSERT/UPDATE/DELETE).
  • execSQLNoJsonReturn($sql,$conditions) β€” prepares, executes, returns raw PHP array (used internally to avoid redundant JSON re-encoding).

5. Database Migrations (lib/Migration/) ΒΆ

Each migration file implements the Nextcloud SimpleMigrationStep interface and runs automatically on app update. Files are version-stamped by filename.

MigrationChange
Version7 (2022-01-25)Initial schema: creates configuration, client, devis, produit, produit_devis, facture tables.
Version20002 (2022-02-01)Adds order column to produit_devis; adds path, tva_default, mentions_default to configuration.
Version20006 (2022-02-23)Adds devise, auto_invoice_number to configuration.
Version20007 (2022-03-11)Adds facture_prefixe column to configuration.
Version20104 (2022-07-28)Adds user_id column to devis and facture (for sequential numbering per company).
Version20203 (2023-01-15)Adds status_paiement to facture; adds comment to devis and facture.
Version20204 (2023-02-02)Adds conf_share table (shared company access); adds changelog to configuration.
Version20300 (2023-07-14)Adds vat column to produit; adds header bool to produit_devis (section headers in quotes).
Version20600 (2025-03-26)Adds vat_number, zip_code, city_name, country_code, iban to configuration.
Version20003/04/05 (2026)Adds zip_code, city_name, country_code to client; aligns schema for Factur-X buyer address fields.

6. Controllers (lib/Controller/) ΒΆ

Controllers are thin by design. Each one receives injected services via the DI container and delegates immediately. The only logic present is extracting parameters from the HTTP request.

ControllerPrimary Service(s)Responsibility
ViewControllerTemplateServiceRenders all HTML page views (index, devis, facture, produit, statistique, legalnotice).
ClientControllerDataServiceCRUD for clients: getClients, getClient, getClientbyiddevis, insertClient.
DevisControllerDataService + TemplateServiceReads quotes list, renders devisshow, inserts quote or product line.
FactureControllerDataService + TemplateServiceReads invoices list, renders factureshow, inserts invoice.
ProduitControllerDataServiceReads products list, reads by quote ID, inserts product.
ConfigurationControllerTemplateService + DataService + CompanyServiceRenders config page, checks config state, reads and updates configuration fields.
CrudControllerDataServiceGeneric operations: update, duplicate, drop (reorder), delete β€” works across multiple entity types.
CompanyControllerCompanyServiceMulti-company lifecycle: create, delete, switch active company, manage shared users.
PdfControllerPdfServicePDF generation and delivery: generate, save, send by email, generate Factur-X, submit to Iopole.
StatsControllerDataServiceReturns entity counts and monthly turnover data for the statistics dashboard.
AdminControllerBdd (direct)Single backup endpoint β€” dumps all data as a dated text file in the user's cloud storage.

7. External Dependencies ΒΆ

PackageTypeUsed for
mpdf/mpdfComposer (PHP)HTML-to-PDF rendering for quotes and invoices (A4, UTF-8, custom CSS).
GestionFacturXWriterBundled (Controller/)Embeds Factur-X XML metadata into a PDF file (XMP / PDF/A-3 conformance).
Nextcloud IMailerFrameworkSMTP abstraction for sending PDFs by email.
Nextcloud IDBConnectionFrameworkPDO-based database access; supports SQLite, MySQL, PostgreSQL.
Nextcloud IRootFolderFrameworkUser file storage access (read logos, save PDFs).
Nextcloud IConfigFrameworkRead system and app configuration values (SMTP, Iopole credentials).
Nextcloud ISessionFrameworkStores the active company ID between requests.
Nextcloud IUserManagerFrameworkResolves Nextcloud user objects by email for share management.

Tip

Composer dependencies are locked in composer.lock. Run make appstore to build a production-ready release archive.

← Previous ↑ Back to top Next β†’