Sociedades Inactivas is a Next.js website with a catalogue of already incorporated Spanish companies. A visitor can select a company and ask about it, or send a general enquiry.
The form has two jobs: save the request and notify the people who need it. Email can fail after the database has already saved the message. If that failure makes the visitor start again, the application can receive the same enquiry twice.
The implemented response contract is simpler for the visitor: success means the request has a stored reference. Email delivery has its own state.
One message through a failed notification
This is the failure case handled by the code, rather than a claim that a particular customer lost an enquiry:
| Event | What the application keeps |
|---|---|
| Valid form arrives | Enquiry and pending-delivery record are saved together |
| First email attempt fails | The same enquiry remains saved; delivery stays pending |
| Visitor repeats the same request | The original reference is returned |
| Same request key arrives with changed content | A conflict is returned instead of silently using the old message |
The pending-delivery record is an outbox: work saved in the database so a notification can be retried later. It is created in the same transaction as the enquiry. A transaction makes those writes succeed or roll back together.
The tests force the outbox insert to fail. The enquiry and its rate-limit increments must roll back too, so a saved request cannot be left without its delivery work.
The database commits before the email attempt
- 01Submission keyCheck the key and payload fingerprint
- 02TransactionSave enquiry and delivery record together
- 03SMTP attemptClaim the record with a lease token
- 04Saved referenceReturn it even if email fails
A retry must mean the same request
The browser sends a submission key. The server stores a hash of that key and a fingerprint, a hash representing the validated message content.
Matching key and content return the existing reference before charging the submission quota again. Matching key with different content returns HTTP 409, a conflict. The same key cannot quietly identify two different messages.
A selected company is also checked against the current catalogue inside the transaction. The stored company name comes from that lookup, not arbitrary browser text. If it has been removed or sold, the visitor can remove the selection and send a general enquiry.
What happens after the commit
The API waits for its first email attempt after saving. A mail error is caught without discarding the saved reference.
For retries, a worker first claims the delivery record with a lease token. The lease is a temporary ownership claim, here lasting 60 seconds. Another worker should leave the record alone while that claim is current. A failed attempt is rescheduled with increasing delays, capped at six hours, and the final update checks that the token still matches.
This does not guarantee exactly one email: a mail server may accept a message just before its response is lost, or a lease may expire during a slow send. The enquiry's storage and the notification's final state remain different facts.
Eleven focused tests cover storage, duplicate requests, rollback and delivery behavior using disposable databases and mocked mail. Production activation is a separate unfinished step: the deployment record has the form returning 503 before saving while security and mail configuration were deferred. This note describes the implemented flow and its tests, not successful live enquiries.
Project sources & checks
sociedades_inactivas_new_web: app/api/contact/route.ts, lib/db/contacts.ts, lib/contact/delivery.ts and tests/contact.test.mjs.
All eleven existing contact tests passed locally, using test databases and mocked delivery.
docs/PROJECT-STATE.md records the production contact route returning 503 before storage, with security configuration and mail setup deferred.