This commit is contained in:
parent
aa2327ff1d
commit
2fa6e70af1
10 changed files with 619 additions and 20 deletions
|
|
@ -691,6 +691,72 @@ void RunMoneySelfTest() {
|
|||
Check(ex.find("VAT 0%") != std::string::npos, "invoice: export VAT 0%");
|
||||
Check(ex.find("art. 146") != std::string::npos, "invoice: export legal basis");
|
||||
Check(ex.find("€955.02") != std::string::npos, "invoice: export total");
|
||||
|
||||
// ── the order confirmation email ──────────────────────────────
|
||||
// Same order, EU shape again; the attachment stands in for the
|
||||
// clearsigned invoice — the builder must carry it verbatim.
|
||||
o.vatIncluded = true;
|
||||
o.buyer.country = "NL";
|
||||
o.goodsMinor = 112660;
|
||||
o.shippingMinor = 863;
|
||||
o.totalMinor = 113523;
|
||||
const std::string mail = Server::BuildOrderConfirmationEmail(
|
||||
o, "Fairphone 6", "Forest Green", "Catcrafts <info@catcrafts.net>",
|
||||
"https://catcrafts.net/order/0123456789abcdef0123456789abcdef",
|
||||
"SIGNED-INVOICE-STAND-IN\n", "Fri, 08 Aug 2026 10:00:00 +0000");
|
||||
Check(mail.find("From: Catcrafts <info@catcrafts.net>\n") != std::string::npos,
|
||||
"email: From header");
|
||||
Check(mail.find("To: b@example.org\n") != std::string::npos, "email: To header");
|
||||
Check(mail.find("Subject: Catcrafts order CC-TEST01 confirmed\n") != std::string::npos,
|
||||
"email: subject carries the reference");
|
||||
Check(mail.find("Date: Fri, 08 Aug 2026 10:00:00 +0000\n") != std::string::npos,
|
||||
"email: date header");
|
||||
Check(mail.find("Message-ID: <0123456789abcdef0123456789abcdef@catcrafts.net>\n")
|
||||
!= std::string::npos,
|
||||
"email: message id from the token");
|
||||
Check(mail.find("MIME-Version: 1.0\n") != std::string::npos, "email: mime version");
|
||||
Check(mail.find("multipart/mixed") != std::string::npos, "email: multipart");
|
||||
Check(mail.find("Fairphone 6 — Forest Green × 2") != std::string::npos,
|
||||
"email: item names colour and quantity");
|
||||
Check(mail.find("€1135.23") != std::string::npos, "email: total");
|
||||
Check(mail.find("incl. 21% NL VAT") != std::string::npos, "email: EU VAT wording");
|
||||
Check(mail.find("* Paid via: ideal\n") != std::string::npos, "email: payment method");
|
||||
Check(mail.find("https://catcrafts.net/order/0123456789abcdef0123456789abcdef")
|
||||
!= std::string::npos,
|
||||
"email: order page link");
|
||||
Check(mail.find("filename=\"catcrafts-invoice-"
|
||||
"f57c6512-f012-4b91-adb3-077876480178-7.md\"") != std::string::npos,
|
||||
"email: attachment filename is the invoice number");
|
||||
Check(mail.find("SIGNED-INVOICE-STAND-IN\n") != std::string::npos,
|
||||
"email: attachment body verbatim");
|
||||
Check(mail.find("--=_cc_0123456789abcdef0123456789abcdef--\n") != std::string::npos,
|
||||
"email: multipart closes");
|
||||
Check(mail.find("KVK 78437059") != std::string::npos, "email: footer identity");
|
||||
|
||||
// The export wording mirrors the invoice's VAT treatment.
|
||||
o.vatIncluded = false;
|
||||
o.buyer.country = "CA";
|
||||
o.totalMinor = 95502;
|
||||
const std::string exMail = Server::BuildOrderConfirmationEmail(
|
||||
o, "Fairphone 6", "Forest Green", "Catcrafts <info@catcrafts.net>",
|
||||
"https://catcrafts.net/order/x", "S\n", "Fri, 08 Aug 2026 10:00:00 +0000");
|
||||
Check(exMail.find("zero-rated export") != std::string::npos,
|
||||
"email: export VAT wording");
|
||||
Check(exMail.find("€955.02") != std::string::npos, "email: export total");
|
||||
|
||||
// A single unit does not advertise a quantity.
|
||||
o.quantity = 1;
|
||||
const std::string one = Server::BuildOrderConfirmationEmail(
|
||||
o, "Fairphone 6", "Forest Green", "Catcrafts <info@catcrafts.net>",
|
||||
"https://catcrafts.net/order/x", "S\n", "Fri, 08 Aug 2026 10:00:00 +0000");
|
||||
Check(one.find("Forest Green ×") == std::string::npos, "email: qty 1 stays silent");
|
||||
|
||||
// The last line of defence: an address that could smuggle a header
|
||||
// yields NO message at all, however it got into the record.
|
||||
o.buyer.email = "a@b.example\nBcc: leak@evil.example";
|
||||
Check(Server::BuildOrderConfirmationEmail(
|
||||
o, "F", "", "x", "u", "S", "D").empty(),
|
||||
"email: header-injecting address yields no message");
|
||||
}
|
||||
|
||||
// ── rates loader ──────────────────────────────────────────────────
|
||||
|
|
@ -966,6 +1032,17 @@ int main(int argc, char** argv) {
|
|||
Server::ConfigureInvoicing(v);
|
||||
}
|
||||
|
||||
// Order email: a sendmail-compatible command ("msmtp -t" on the
|
||||
// server) that reads the message on stdin and takes the recipient
|
||||
// from its headers. Unset means no email is sent — the order page
|
||||
// and invoice download remain the buyer's receipt.
|
||||
{
|
||||
Server::MailConfig mailCfg;
|
||||
if (const char* v = std::getenv("MAIL_COMMAND"); v && *v) mailCfg.command = v;
|
||||
if (const char* v = std::getenv("MAIL_FROM"); v && *v) mailCfg.from = v;
|
||||
Server::ConfigureMail(std::move(mailCfg));
|
||||
}
|
||||
|
||||
// Sendcloud is optional: without credentials the compiled-in zone
|
||||
// table prices all shipping, which is exactly how dev and e2e run.
|
||||
// With credentials the refresh thread fetches per-country rates.
|
||||
|
|
@ -1042,6 +1119,7 @@ int main(int argc, char** argv) {
|
|||
" --orders [FILE] [--mark-paid TOKEN | --mark-shipped TOKEN | --cancel TOKEN]\n"
|
||||
"\n"
|
||||
"environment: MOLLIE_API_KEY (test_… or live_…), BUNQ_API_KEY, BUNQ_SANDBOX=1,\n"
|
||||
" ORDER_REDIRECT_BASE, SENDCLOUD_PUBLIC_KEY/SECRET_KEY/METHOD");
|
||||
" ORDER_REDIRECT_BASE, SENDCLOUD_PUBLIC_KEY/SECRET_KEY/METHOD,\n"
|
||||
" INVOICE_GPG_KEY, MAIL_COMMAND (e.g. 'msmtp -t'), MAIL_FROM");
|
||||
return 0;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue