This commit is contained in:
parent
aa2327ff1d
commit
2fa6e70af1
10 changed files with 619 additions and 20 deletions
|
|
@ -38,11 +38,12 @@ export namespace Catcrafts::Server {
|
|||
|
||||
// ── orders ────────────────────────────────────────────────────────
|
||||
//
|
||||
// An append-only JSON-lines EVENT LOG, not a database. Two event types:
|
||||
// "order" (the full record, written once) and "status" (a transition).
|
||||
// Current state is a fold over the file — later events win. Nothing is
|
||||
// ever rewritten in place, so the file is also the audit trail, and a
|
||||
// crash mid-append costs at most the line being written.
|
||||
// An append-only JSON-lines EVENT LOG, not a database. Four event types:
|
||||
// "order" (the full record, written once), "status" (a transition),
|
||||
// "invoice" (the number assignment) and "notified" (the confirmation
|
||||
// email left). Current state is a fold over the file — later events win.
|
||||
// Nothing is ever rewritten in place, so the file is also the audit
|
||||
// trail, and a crash mid-append costs at most the line being written.
|
||||
//
|
||||
// The volume argument: this sells single-digit units per week. When that
|
||||
// is wrong by two orders of magnitude, the log imports into SQLite in one
|
||||
|
|
@ -69,6 +70,8 @@ export namespace Catcrafts::Server {
|
|||
std::string paidVia; // method that settled it ("ideal", "creditcard")
|
||||
std::string invoiceNumber; // "<customer-uuid>-<n>", set at paid
|
||||
std::string invoicedAt; // ISO 8601 of the invoice event
|
||||
std::string confirmationSentAt; // ISO 8601 of the confirmation-email
|
||||
// event; empty = not (yet) emailed
|
||||
};
|
||||
|
||||
void SetOrdersPath(const std::filesystem::path& path);
|
||||
|
|
@ -95,12 +98,28 @@ export namespace Catcrafts::Server {
|
|||
std::optional<std::string> AssignInvoiceNumber(std::string_view token,
|
||||
std::string_view isoTimestamp);
|
||||
|
||||
// Appends the notified event: this order's confirmation email was
|
||||
// accepted by the mail command. Written AFTER the handoff succeeds, so a
|
||||
// crash between send and append errs toward a duplicate email — an
|
||||
// apology — never toward a buyer who paid and heard nothing.
|
||||
bool AppendOrderNotified(std::string_view token, std::string_view isoTimestamp);
|
||||
|
||||
// ── invoices ──────────────────────────────────────────────────────
|
||||
//
|
||||
// A paid order's invoice: plain markdown, clearsigned with the shop's
|
||||
// GPG key so its authenticity outlives this server. The page invites the
|
||||
// buyer to download it rather than promising to host receipts forever.
|
||||
|
||||
// The registered business identity — on every invoice and at the foot of
|
||||
// every order email. One definition, like the rest of the compiled-in
|
||||
// authored content; the selftest pins the values.
|
||||
inline constexpr std::string_view kSellerName = "Catcrafts";
|
||||
inline constexpr std::string_view kSellerStreet = "Chico Mendesring 256";
|
||||
inline constexpr std::string_view kSellerCity = "3315NN Dordrecht";
|
||||
inline constexpr std::string_view kSellerKvk = "78437059";
|
||||
inline constexpr std::string_view kSellerVat = "NL003329281B38";
|
||||
inline constexpr std::string_view kSellerSite = "catcrafts.net";
|
||||
|
||||
// Pure and exported for the self-test: everything on a Dutch invoice —
|
||||
// seller identity (KVK/VAT), sequential number, dates, buyer address,
|
||||
// per-line amounts, VAT treatment for EU and export.
|
||||
|
|
@ -125,6 +144,44 @@ export namespace Catcrafts::Server {
|
|||
std::string NewOrderToken();
|
||||
std::string ReferenceFromToken(std::string_view token);
|
||||
|
||||
// ── the order confirmation email ──────────────────────────────────
|
||||
//
|
||||
// A paid order gets ONE email: the confirmation, with the clearsigned
|
||||
// invoice attached. Delivery shells out to a sendmail-compatible command
|
||||
// (msmtp -t on the server) exactly as signing shells out to gpg — TLS,
|
||||
// AUTH and deliverability are the parts a hand-rolled SMTP client
|
||||
// reimplements badly, and this sends a handful of messages per week.
|
||||
// The command reads the complete RFC 5322 message on stdin and takes the
|
||||
// recipient from its headers; the buyer's address was validated at
|
||||
// checkout to be header-safe (Form::LooksLikeEmail rejects CR, LF,
|
||||
// commas and angle brackets for exactly this moment).
|
||||
|
||||
struct MailConfig {
|
||||
std::string command; // MAIL_COMMAND, e.g. "msmtp -t"; empty = no email
|
||||
std::string from; // MAIL_FROM header; defaults to the shop inbox
|
||||
};
|
||||
void ConfigureMail(MailConfig config);
|
||||
bool MailConfigured();
|
||||
// The configured From header — the mailer passes it to the builder.
|
||||
std::string MailFrom();
|
||||
|
||||
// Pure and exported for the self-test: the complete MIME message, a
|
||||
// plain-text confirmation plus the invoice as a markdown attachment.
|
||||
// Returns an empty string when the buyer's address fails the envelope
|
||||
// shape check — the last line of defence sits where the envelope is
|
||||
// built, not in the history of the record.
|
||||
std::string BuildOrderConfirmationEmail(const OrderRecord& order,
|
||||
std::string_view productName,
|
||||
std::string_view colorLabel,
|
||||
std::string_view from,
|
||||
std::string_view orderUrl,
|
||||
std::string_view invoiceAttachment,
|
||||
std::string_view dateRfc2822);
|
||||
|
||||
// Pipe one message into the configured command. False means "not sent,
|
||||
// keep it queued": the mailer never writes a notified event on failure.
|
||||
bool SendMailMessage(const std::string& message);
|
||||
|
||||
// ── payments ──────────────────────────────────────────────────────
|
||||
//
|
||||
// A rail turns "this order wants €X" into a URL a buyer can pay at, and
|
||||
|
|
|
|||
Loading…
Reference in a new issue