Japanese tax documents, issued in a chat window
A customer asks for an invoice in the messaging app they already have open. A conversation collects what is missing, a PDF is generated with the right type and the right stamp, filed to the finance team's Drive and backed up to object storage, and the link comes back in the same thread. There is no web portal, on either side of the transaction, and that is the design rather than the roadmap.
- Surface
- LINE, in Japanese
- Documents
- 請求書 and 領収書
- Runtime
- Python on Lambda
- Tests
- 1,085 across three layers
The back office is the chat window too
Issuing an invoice or a receipt is a small, frequent, deadline-shaped task. The usual answer is an admin panel: someone logs in, finds the customer, fills a form, downloads a PDF, emails it. Every step of that is a place to be blocked, and the panel itself is a product somebody has to build, secure and maintain.
So there is no panel. Operators work inside the same chat product the customers use. They pick up a conversation that has stalled, see which step it stalled on, handle it, and hand it back, and the customer never learns that a human stepped in. An admin web interface is not on the roadmap for later; it is a thing this system decided not to have.
That decision is what makes the rest of the architecture legible. The messaging platform is the interface, so there is no frontend. The whole application is a request handler behind a webhook, which means it can be one function, which means the operational surface is small enough that a small team can actually own it.
From a sentence to a filed PDF
- 01
A message, not a form
The customer asks for an invoice or a receipt in the chat they already use. An intent router reads the message and hands it to the matching document flow, which then asks only for what it is still missing.
- 02
The PDF is generated, not filled in
Japanese type is embedded in the document rather than assumed present on the renderer, and the company stamp is composited in. A tax document that renders with substituted glyphs on the recipient's machine is not a tax document.
- 03
Filed twice, in different places
Every document goes to the shared Drive the finance team already works out of, and to an S3 bucket they never touch. The issuance row records both locations, so the backup is addressable rather than theoretical.
- 04
The link comes back in the chat
The reply carries the Drive link. Nobody logs into anything, and there is no separate portal to keep credentials for.
Test functions across unit, database and deployed-environment layers
Independent copies filed of every document issued
Tables in one versioned schema, applied on cold start
Terraform files: no infrastructure clicked into a console
Three rails, and an honest count of them
Documents are only half of it. Payment confirmation arrives from a card processor and from an international payments provider, each through its own signed webhook, and reconciles against the issued document rather than against a spreadsheet.
A third rail reads bank transfers directly through a Japanese banking API, matching an inbound transfer to the reference code quoted on the invoice. That one is built and tested against the sandbox, and is not yet approved for production. Two rails are live, one is waiting on somebody else’s compliance queue, and it would be easy to write “three payment integrations” on a slide and let you assume otherwise.
A fourth processor was removed. Its routes are gone, and a test asserts they stay gone, because a decommissioned payment path that quietly still answers is a much worse problem than one that obviously does not.
Deliberately small infrastructure
The request path is a CDN for TLS, a load balancer locked by security group to that CDN’s address ranges, one function, and a small single-availability-zone database with no connection proxy in front of it. For a system handling money, that reads under-engineered until you look at the actual load, which is a steady trickle rather than a spike.
The bank reconciler is an optimisation, not a dependency. Without it, confirming payments by hand costs an operator a couple of minutes a day. Knowing that changes what the automation is for: it is convenience, so it is allowed to be pending approval, and nothing stops while it is. A system that had been designed as though the reconciler were load-bearing would have been more expensive to build and would now be blocked.
Everything is Terraform, including the cost budgets. Fifteen files covering the network, the database, the keys, the buckets, the monitoring and the bastion, so the environment is reproducible and the bill has an alarm on it.
Three failures designed against
Signatures are verified before anything else. Every webhook payload has its signature checked with a constant-time comparison before a single line of business logic runs. Not after parsing, not after logging, not after looking up the customer. Anything that touches state behind an unverified signature is an endpoint anyone on the internet can drive.
Bundled files fail quietly, so they have a test. The schema and the fonts and the stamp all ship inside the deployment package and are resolved relative to the package root rather than to the importing module. Move a package and that resolution changes silently: the schema initialiser only logs when it cannot find the schema, so migrations would simply stop applying while everything appeared to work. A unit test guards every one of those paths, because the failure mode is invisible rather than loud.
Test traffic cannot pollute real filing. Documents issued to test and QA identities are forced to carry a [TEST] filename prefix, which is what makes automated cleanup safe to point at the same Drive folder and buckets the real documents live in. Without a marker that cannot be forgotten, either the cleanup is unsafe or the tests never run against the real thing.
Got a process living in somebody's inbox?
Document issuance, approvals, reconciliation: the useful version of this conversation starts with whichever one currently depends on a specific person being at their desk.
hello@littlelabrats.com