NF525 audit
Treatwell internal use only!
This documentation contains Treatwell internal information, it is not to be shared during the audits.
The scripts and commands in the following section are intended to be executed through the Rails console on the audit environment (i.e. using the toolbox pod of the testing environment).
Audits are never executed on the production environment, but you know, pay attention to not mess up things...
Preparation
Create a test venue
During the audit we need to have a "fresh" venue, i.e. a venue without any checkout done.
The way we are using is to run the Rake task e2e:venue_generator.
This task is intended for E2E testing purposes, so we are kind of abusing it here.
The same script can be run using the Generate test venue for button in the Active Admin venues section (top right), i.e. for staging here.
The created venue will miss some associated stuff, it may be useful to have it available before the audit begins.
We will probably need:
- one product in stock.
Ruby instructions to create a single product for a given venue
VENUE_ID = exit # SET THE VENUE ID
venue = Venue.find(VENUE_ID); nil
manufacturer = Manufacturer.create!(
name: "Some Manufacturer #{SecureRandom.hex(4)}",
official: true,
short_name: "M#{SecureRandom.hex(2)}"
)
product = Product.create!(
name: "Some product",
manufacturer: manufacturer,
official: true,
item_number: SecureRandom.alphanumeric(9).upcase
)
VenueProduct.create!(
product: product,
venue: venue,
name: product.name,
stock: 10,
minimum_stock: 3,
minimum_reorder: 6,
buying_price: Money.new(1000, venue.currency),
selling_price: Money.new(2000, venue.currency)
)
- one business customer to issue invoices
Ruby instructions to create a single business customer for a given venue
VENUE_ID = exit # SET THE VENUE ID
venue = Venue.find(VENUE_ID)
venue.customers.create!(
full_name: 'Some Customer',
vat_number: 'FR03849382243',
siret_code: '73282932000074',
company_name: 'Some Company SARL',
business_customer: true
)
- one marketing promotion to sell to customers
Ruby instruction to create a prepaid credit marketing promotion for a given venue
VENUE_ID = exit # SET THE VENUE ID
venue = Venue.find(VENUE_ID)
venue.marketing_promotions.create!(
marketing_promotion_type: 'prepaid_credit',
name: 'Some Marketing Promotion',
valid_in_time_table: (['00:00,23:59'] * 7).join("\n"),
redemption_amount_possible_cents: 10000,
redemption_amount_used_cents: 0,
country: venue.country
)
Audit time
During the audit some engineer will be required to perform some activities to show the compliance with the requirements.
Generate the archives
It happened sometimes that the auditor asked to create new checkouts after the archives generation.
Don't try to do it at home! The system has no direct mean to do so, and trying to delete the data by hand would break signatures everywhere (actually they are there exactly for this purpose). So, given that, if you want to create new checkouts after the fiscal day is closed and the archive generated, create a new venue! Auditors have shown to understand it quite easily.
Archives are usually created automatically each month as described by the Archives documentation. The following paragraphs describe how to create them manually using the Rails console.
It will be necessary to generate the monthly archive for the current month (it will contain the information about the checkouts created during the audit) and the yearly archive for the current year.
The fiscal archives for each venue can be found on AA (i.e. https://staging-admin.uala.it/backend/venues/5/fiscal_archives).
Other than the method described below, archives can also be generated manually using the Rake task cash:nf525_generate_yearly_fiscal_archive.
It generates one monthly archive for the current month, and one yearly archive for the current year.
In past audits it showed to be confusing for auditors to see yearly archives containing only one monthly archive. Therefore, the method describe in the following paragraphs is recommended.
The Rake task can be useful for testing purposes.
Monthly fiscal archive
After the current fiscal day has been closed from the cash UI, run the following script to create the monthly fiscal archive.
It generates the daily and monthly summary. Then generation of the monthly summary also generates the montly archive.
Ruby instructions to generate the monthly archive for a given venue
VENUE_ID = exit # SET THE VENUE_ID
venue = Venue.find(VENUE_ID); nil
Venues::GenerateFiscalSummaryInteractor.call(venue_id: venue.id, input_date: Date.current) # for the checkouts created during the audit test
Venues::GenerateFiscalSummaryInteractor.call(venue_id: venue.id, input_date: Date.current.end_of_month)
Yearly archive
After the archive for the current month as been generated, run the following script to generate the yearly archive.
It generates all the (empty) archives from the next month to December of the current year, they will be needed to create the yearly archive.
Ruby instructions to generate the yearly fiscal archive for a given venue
VENUE_ID = exit # SET THE VENUE_ID
venue = Venue.find(VENUE_ID); nil
((Date.current.month + 1)..12).each do |month|
date = Date.new(Date.current.year, month).end_of_month
Venues::GenerateFiscalSummaryInteractor.call(venue_id: venue.id, input_date: date)
end
Validate the generated archives
The public key for the NF525 archive validation is stored as a country provider with type nf525_signature and name public_key, i.e. here for staging.
As of July 7th 2026, this application lives inside the Treatwell VPN. From the requirements it is not clear if it is legit or not. It is reasonable to assume that it isn't. Moving the app outside the VPN would imply a lot of work to address a lot of security concerns. Given the amount of time the app is used (exactly one a year, during the audit), it is probably not worth the hassle.
But.. don't mention the VPN stuff during the audit. Have your Treatwell VPN ready!
Once the archives have been generated, we are going to be asked to show the validation tool we make available to validate the archives we generate.
The tool is available at https://fiscal-validator.twpro.twtools.io.
It's source code can be found at https://git.treatwell.net/dev/zip-sign-checker.
This application is also used by Connect. So be careful when make changes there to not break Connect stuff.
The application form is prefilled with the public key for the production environment.
To make it work:
- upload the archive file
- copy the public key associated with the environment where the archive where generated
- smash the button and, if everything works, see the archive getting validated
The same tool works for both the monthly and yearly archives.
Failing validation
We may be required to show that a tampered archive is not validated by the application.
In this case the following procedure saved the day that one time we were asked to do so:
- unzip the montly archive that was generated
- change any value inside one of the CSV file (choose some value from the grand total files for a granted WOW effect)
- compress the archive again, paying attention to not change the directory structure inside the file
- perform the validation with the tampered file, and see the archive getting rejected
Following is a example session of shell commands used to tamper with a test archive.
This is just an example and should not be executed as a script without thinking.
Shell commands to tamper with archive files
export ARCHIVE_NAME='monthly_archive_venue_38843_20260731'
# Step 1. unzip the monthly archive
# Unzip the container file
mkdir test_archive_nf525
cd test_archive_nf525
unzip ~/Downloads/${ARCHIVE_NAME}_with_digest.zip
# Unzip the actual archive
mkdir ${ARCHIVE_NAME}
cd ${ARCHIVE_NAME}
unzip ../${ARCHIVE_NAME}.zip
# Step 2. change values
# You can use any suitable editor for CSV files. Here I'm using `vim`.
vim fiscal_summaries.csv
# Step 3. zip the archive back
# Zip the actual archive
zip ../${ARCHIVE_NAME}.zip *
# Cleanup tampered files
cd ..
rm -fr ${ARCHIVE_NAME}
# Zip the container file
zip ~/Downloads/${ARCHIVE_NAME}_with_digest_TAMPERED.zip *
# Step 4. Validate the tampered archive, you cannot do it with the shell.
It could get more complicated than that, for example by signing the tampered archive with another pair of keys or whatever...
Show database internals
We will be required to show how the data is actually stored in the database and in the archive files. The database connection information is split between the overlay's env file and AWS Secrets Manager:
- host: in
backend-main-vars.envin the environment's overlay ink8s-gitops-twpro(see staging). Look for the variableREALTIME_REPLICA_DATABASE_URLon staging orDATABASE_URLon dev. - database name, username and password: in the
backend-main-database-secretssecret in AWS Secrets Manager,eu-central-1—twpro/staging/shared/backend-main-database-secretsfor staging. The keys arePOSTGRESQL_DATABASE, plusAURORA_USERNAMEandAURORA_PASSWORDon staging orPOSTGRESQL_USERNAMEandPOSTGRESQL_PASSWORDon dev. Reading them needs thetwpro-secretspermission set — see Deployment secrets flow.
The models (and therefore database tables) involved with the certification are described in the Archive content documentation.
Show archive internals
Download the monthly archive file generated during the audit and open it with your favorite ZIP extractor. That's it.
What you are going to find inside the archvive is described in the Archives documentation.
Show signature code
The main interest is the class Hairdressr::Signable::Nf525 where the actual signatures on the records are computed.
The fields used for signature generation for each kind of record can be found in uala-main/lib/hairdressr/signable/nf525.
The code used to sign the actual archive ZIP file is in Venues::Nf525::GenerateMonthlyFiscalArchiveInteractor, search for the Hairdressr::Signable::Nf525::Signer.
For more information about signatures, take a look at the Signatures documentation.
Show the types of JET events we handle
Sometimes we were asked to show tha types of JET events our system can handle.
They can be found in TechicalEventLogs::Validations