<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Badreddine Oussaih</title>
    <description>The latest articles on DEV Community by Badreddine Oussaih (@badr0h).</description>
    <link>https://dev.to/badr0h</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4037930%2Fa98735d6-6d1b-437a-b111-23807e3dcaa7.png</url>
      <title>DEV Community: Badreddine Oussaih</title>
      <link>https://dev.to/badr0h</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/badr0h"/>
    <language>en</language>
    <item>
      <title>Credit Notes and Refunds in a Multi-Currency Ledger: Which Exchange Rate Actually Applies?</title>
      <dc:creator>Badreddine Oussaih</dc:creator>
      <pubDate>Tue, 11 Aug 2026 13:46:54 +0000</pubDate>
      <link>https://dev.to/badr0h/credit-notes-and-refunds-in-a-multi-currency-ledger-which-exchange-rate-actually-applies-236b</link>
      <guid>https://dev.to/badr0h/credit-notes-and-refunds-in-a-multi-currency-ledger-which-exchange-rate-actually-applies-236b</guid>
      <description>&lt;p&gt;Introduction&lt;/p&gt;

&lt;p&gt;Most accounting systems handle the happy path of a foreign-currency invoice reasonably well: record the amount, snapshot the exchange rate at invoice date, convert to the base currency for reporting, done. Where that same system usually falls apart is the correction path — what happens when that invoice needs to be cancelled, partially refunded, or adjusted weeks or months later. The question sounds trivial (what exchange rate does the credit note use?) but the answer determines whether your ledger stays internally consistent or slowly accumulates untraceable noise.&lt;/p&gt;

&lt;p&gt;This post covers why that question is harder than it looks, the mistakes that show up when it's handled carelessly, and a data model that keeps corrections predictable — including how Comptyx applies it in practice.&lt;/p&gt;

&lt;p&gt;The business problem&lt;/p&gt;

&lt;p&gt;A credit note (or refund, or invoice cancellation) is, in most cases, meant to be the mirror image of the transaction it corrects: if an invoice added a debit and a credit to specific accounts at a specific rate, the credit note should remove exactly that, not something numerically close to it. But most systems don't treat it that way by default. Instead, they treat a credit note as a new transaction, valued at whatever exchange rate is current on the day it's issued.&lt;/p&gt;

&lt;p&gt;For a domestic business, this distinction doesn't matter — the "rate" is always 1:1. The moment a business invoices across currencies, it matters a great deal. A EUR 10,000 invoice to a US client, booked when EUR/USD was 1.08, is worth $10,800 at invoice time. If that invoice is cancelled eight weeks later and the credit note is valued at the rate in effect that day — say EUR/USD 1.11 — the credit note reverses $11,100, not the $10,800 that was actually booked. The ledger now shows a $300 discrepancy that isn't a real gain or loss from currency movement; it's an artifact of using the wrong rate for a reversal.&lt;/p&gt;

&lt;p&gt;Why it matters&lt;/p&gt;

&lt;p&gt;This kind of error is easy to dismiss as immaterial on a single transaction, and it usually is. The problem is that it's systematic, not random: every cancellation or refund of a foreign-currency invoice, using a naive "current rate" approach, introduces a discrepancy in the same direction as whatever the currency has moved since the original invoice. Over a year, across dozens or hundreds of corrections, that adds up to a real distortion in reported FX gain/loss — and it's one that doesn't correspond to any actual currency exposure the business had, because the underlying transaction was cancelled, not held.&lt;/p&gt;

&lt;p&gt;It also creates a reconciliation problem. If VAT or sales tax was reported on the original invoice at one rate, and the credit note recalculates the tax adjustment at a different rate, the correction doesn't tie back cleanly to what was originally filed — which is exactly the kind of mismatch a tax authority query surfaces.&lt;/p&gt;

&lt;p&gt;Common mistakes&lt;/p&gt;

&lt;p&gt;Treating every credit note as a fresh transaction. The simplest implementation — look up today's rate, apply it — is also usually wrong, because it silently converts a reversal into a partially new economic event.&lt;/p&gt;

&lt;p&gt;Not distinguishing "reversal" from "new adjustment." Not every credit note is a pure reversal. A partial refund negotiated at a renegotiated price weeks later is arguably a new event and might legitimately warrant a new rate. Systems that don't let a user express that distinction force every correction into one bucket, which is wrong for some of them either way.&lt;/p&gt;

&lt;p&gt;Deriving the rate from a rate table instead of a stored snapshot. If the original invoice's rate wasn't stored explicitly on the transaction and instead has to be looked up from a historical rate table at the time the credit note is processed, any gap or correction in that table changes the outcome of past reversals — a rate table update today can silently alter how a six-month-old credit note is valued.&lt;/p&gt;

&lt;p&gt;No audit trail explaining which rate was used and why. Even when the rate choice is correct, if there's no record of whether a given credit note used the original or a new rate, and why, it's difficult to defend the number later during a review or audit.&lt;/p&gt;

&lt;p&gt;Better workflow&lt;/p&gt;

&lt;p&gt;The fix is mostly about what gets stored, not what gets calculated. Three things need to be true of the data model:&lt;/p&gt;

&lt;p&gt;First, every invoice needs its exchange rate stored as an immutable snapshot at the transaction itself, not as a reference to a rate table that can change. Second, a credit note needs an explicit flag for whether it's a pure reversal (reuse the original invoice's snapshot rate) or a new adjustment (use a new rate, stored with its own snapshot and justification). Third, any FX gain or loss that results from a genuine new-rate adjustment should post to its own visible line, separate from the reversal itself, so it's clear which part of the number is a real currency effect and which part is bookkeeping.&lt;/p&gt;

&lt;p&gt;With that structure, a pure cancellation nets to exactly zero against the original invoice — no residual FX noise — and a genuine new-value adjustment shows its FX impact explicitly instead of hiding it inside the reversal amount.&lt;/p&gt;

&lt;p&gt;How Comptyx solves part of the problem&lt;/p&gt;

&lt;p&gt;Comptyx, a cloud accounting platform for cross-border businesses built on a Java/Spring Boot backend with PostgreSQL, stores the exchange rate as part of the invoice record at the moment it's created, not as a derived value. Credit notes default to reusing that stored rate for straightforward cancellations, so a reversal nets to zero against the original entry with no incidental FX gain or loss. Where a credit note reflects a genuine new event — a partial refund at a renegotiated amount, for example — the rate can be set explicitly, and the resulting FX impact is recorded as its own line rather than absorbed into the reversal. The same underlying transaction data flows into DATEV, Xero, and QuickBooks exports, so the distinction between reversal and adjustment survives the handoff to an accountant working in those systems.&lt;/p&gt;

&lt;p&gt;This doesn't remove the judgment call of whether a specific credit note should be treated as a pure reversal or a new event — that's a decision for the business and its accountant. What it removes is the risk of that decision being made implicitly, by whatever rate happens to be current on the day someone clicks "issue credit note."&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;Credit notes on foreign-currency invoices look like a minor edge case until you notice how often they happen and how consistently a naive implementation gets the exchange rate wrong. The fix isn't complicated — snapshot rates at the source, distinguish reversals from new adjustments, and keep any real FX effect visible rather than buried — but it has to be a deliberate modeling decision, because the default behavior in most systems gets it wrong quietly.&lt;/p&gt;

&lt;p&gt;CTA&lt;/p&gt;

&lt;p&gt;If you want to see how Comptyx models invoice and credit note exchange rates in practice, you can explore it at &lt;a href="https://www.comptyx.com/" rel="noopener noreferrer"&gt;https://www.comptyx.com/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>backend</category>
      <category>database</category>
      <category>fintech</category>
      <category>systemdesign</category>
    </item>
    <item>
      <title>CSV Is Not a Simple Format: The Parsing Mistakes That Cost Teams Real Time</title>
      <dc:creator>Badreddine Oussaih</dc:creator>
      <pubDate>Thu, 23 Jul 2026 09:19:40 +0000</pubDate>
      <link>https://dev.to/badr0h/csv-is-not-a-simple-format-the-parsing-mistakes-that-cost-teams-real-time-3e99</link>
      <guid>https://dev.to/badr0h/csv-is-not-a-simple-format-the-parsing-mistakes-that-cost-teams-real-time-3e99</guid>
      <description>&lt;p&gt;Introduction&lt;/p&gt;

&lt;p&gt;CSV has a reputation problem. Because it looks like plain text with commas in it, it gets treated as the format you don't need to think about — the "just split on comma" format, the thing you reach for when JSON feels like overkill. That reputation is exactly why CSV bugs are so common: teams under-invest in handling it correctly, and the format has just enough edge cases to punish that.&lt;/p&gt;

&lt;p&gt;This post covers where CSV actually breaks in production, why it happens, and what a more reliable approach looks like — whether you're building the export, the import, or both.&lt;/p&gt;

&lt;p&gt;The Problem&lt;/p&gt;

&lt;p&gt;CSV (Comma-Separated Values) has no single formal standard that every tool agrees on. RFC 4180 exists and describes a reasonable common denominator, but plenty of real-world files predate it, ignore it, or extend it (semicolon delimiters in European locales, tab-separated variants, BOM markers at the start of the file). So "parsing CSV" isn't really one task — it's parsing whatever dialect the specific file in front of you happens to use, and guessing wrong is silent, not loud.&lt;/p&gt;

&lt;p&gt;The core mismatch: CSV represents tabular data as text, with exactly one mechanism (quoting) to handle values that contain the delimiter, a newline, or a quote character itself. That mechanism is easy to implement badly, and most hand-rolled parsers implement it badly without realizing it, because their test file never happened to contain a comma inside a field.&lt;/p&gt;

&lt;p&gt;Why It Happens&lt;/p&gt;

&lt;p&gt;Most CSV bugs start the same way: someone needs to read a file quickly, sees that it's comma-separated, and reaches for a one-liner like line.split(',') instead of a real parser. It works immediately, ships, and looks correct — because the sample data used to build and test it didn't contain the specific combination of quotes, embedded commas, or line endings that break it. Weeks or months later, someone else's export (from a different tool, a different locale, or a different spreadsheet app) hits the gap, and the resulting bug looks like a data problem rather than a parsing problem, so it takes longer to trace back to the real cause.&lt;/p&gt;

&lt;p&gt;The same pattern shows up on the writing side: code that builds a CSV file by joining strings with commas instead of using a CSV writer will happily produce a broken file the first time a field contains a comma, and nothing will flag it until a human — or another system — tries to read that file back.&lt;/p&gt;

&lt;p&gt;Common Mistakes&lt;br&gt;
Splitting on comma instead of using a parser. This is the single biggest source of CSV bugs. It works until a field contains the delimiter, at which point every downstream column silently shifts.&lt;br&gt;
Ignoring quoted-comma and escaped-quote rules. "Smith, John" is one field, not two. "She said ""hi""" is one field containing a literal quote. A parser needs to handle both; .split() handles neither.&lt;br&gt;
Assuming a single line-ending convention. \r\n vs \n vs \r differences leave stray characters in field values that break equality checks and lookups downstream.&lt;br&gt;
Assuming UTF-8 without checking. Files exported from older systems or non-English locales are frequently Latin-1, Windows-1252, or UTF-8 with a BOM. Reading with the wrong encoding either throws or silently corrupts non-ASCII characters.&lt;br&gt;
Letting type inference happen implicitly. Leading zeros (ZIP codes, account numbers), values that look like dates but aren't, and numbers with locale-specific decimal separators (1.234,56 in many European locales) all get mangled by tools that guess types instead of respecting an explicit schema.&lt;br&gt;
No round-trip or schema validation. Writing a CSV and reading it back with a different tool (or the same tool with different settings) is the cheapest way to catch dialect mismatches before they reach production.&lt;br&gt;
A Better Approach&lt;/p&gt;

&lt;p&gt;None of this requires exotic tooling — it requires treating CSV as a real format instead of a shortcut:&lt;/p&gt;

&lt;p&gt;Always use a proper CSV library (csv in Python, PapaParse or csv-parse in JavaScript, OpenCSV/Jackson CSV in Java) for both reading and writing. Never build or parse CSV with manual string splitting.&lt;br&gt;
Detect or explicitly declare the dialect (delimiter, quote character, encoding) rather than assuming RFC 4180 defaults, especially for files from external partners or exported from spreadsheet software.&lt;br&gt;
Validate types explicitly against an expected schema instead of trusting automatic type inference, particularly for IDs, ZIP codes, and anything with meaningful leading zeros.&lt;br&gt;
Normalize line endings and encoding on ingest, once, at the boundary of your system, so the rest of your code never has to think about it.&lt;br&gt;
Offload conversion and validation to a tool built for it when CSV is just one hop in a larger pipeline (CSV → JSON for an API, CSV → YAML for config, CSV → SQL for a bulk import) rather than maintaining bespoke parsing logic for every direction you need.&lt;br&gt;
How ZenithConvert Helps&lt;/p&gt;

&lt;p&gt;This is the kind of problem ZenithConvert exists to remove from your plate. It's an AI format converter that handles CSV alongside JSON, XML, YAML, SQL, and close to 100 format pairs in total, dealing with the quoting, escaping, and type-ambiguity issues covered above instead of leaving you to special-case them file by file. For a one-off export you need to reshape, the free online converter handles it in the browser; for recurring pipelines — nightly imports, data migrations, CI steps — the REST API (with JS and Python SDKs) lets you drop conversion into an automated step instead of maintaining a parser by hand. Guest access covers 3 conversions/day with no signup, a free account raises that to 5/day, and the API is available on paid plans for teams doing this at volume.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;CSV earns its bad reputation not because the format is inherently broken, but because it's so easy to under-engineer. A file that "looks simple" hides just enough ambiguity — quoting, encoding, line endings, implicit typing — to turn a five-minute script into a recurring source of silent data corruption. Treating CSV with the same care as any other structured format, using a real parser, and offloading conversion between formats to something purpose-built pays for itself the first time it prevents a 4am "why is this ZIP code missing its leading zero" page.&lt;/p&gt;

&lt;p&gt;CTA: Tell us which format pair gives you the most CSV headaches — we're always looking for what to harden next.&lt;/p&gt;

</description>
      <category>backend</category>
      <category>programming</category>
      <category>softwaredevelopment</category>
      <category>softwareengineering</category>
    </item>
    <item>
      <title>Why Your JSON-to-XML Migration Keeps Breaking (and How to Actually Fix It)</title>
      <dc:creator>Badreddine Oussaih</dc:creator>
      <pubDate>Wed, 22 Jul 2026 21:56:45 +0000</pubDate>
      <link>https://dev.to/badr0h/why-your-json-to-xml-migration-keeps-breaking-and-how-to-actually-fix-it-83m</link>
      <guid>https://dev.to/badr0h/why-your-json-to-xml-migration-keeps-breaking-and-how-to-actually-fix-it-83m</guid>
      <description>&lt;p&gt;Introduction&lt;/p&gt;

&lt;p&gt;If you've worked on a system old enough to have "legacy" in its name, there's a good chance XML is involved somewhere. SOAP APIs, enterprise config files, old EDI pipelines, ancient Java frameworks that predate JSON's popularity — XML is still everywhere in backend systems, even as nearly everything new defaults to JSON. At some point, most teams end up needing to migrate data between the two, and it's rarely as clean as it looks on paper.&lt;/p&gt;

&lt;p&gt;This post walks through why that migration is harder than it seems, the mistakes that show up most often, and a more reliable approach for doing it without losing data or introducing subtle bugs.&lt;/p&gt;

&lt;p&gt;The Problem&lt;/p&gt;

&lt;p&gt;On the surface, JSON and XML both represent structured, hierarchical data, so converting between them feels like it should be mechanical — swap tags for keys, done. In practice, XML supports several concepts that JSON has no native equivalent for: attributes vs. child elements, mixed content (text and elements interleaved), namespaces, and document ordering that sometimes carries semantic meaning. JSON has none of that. It only has objects, arrays, strings, numbers, booleans, and null.&lt;/p&gt;

&lt;p&gt;That mismatch means a naive converter has to make judgment calls, and different tools make them differently. One converter turns every XML attribute into a JSON key prefixed with @. Another drops attributes into a _attributes sub-object. A third just merges them into the parent object and silently overwrites a child element that happens to have the same name. None of these are "wrong" exactly, but if your downstream code expects one convention and gets another, you get a bug that looks like a parsing issue but is actually a modeling disagreement.&lt;/p&gt;

&lt;p&gt;Why It Happens&lt;/p&gt;

&lt;p&gt;Most of these migrations start as "just write a script." Someone grabs an XML parsing library, walks the tree, and builds a JSON object as they go. It works on the sample file they tested with. It ships. Then, three months later, someone hits a document with a repeated element that should become an array but instead overwrites itself, or a namespace prefix that breaks a downstream if statement checking key names, or a numeric-looking string that XML explicitly typed as text but JSON happily coerces to a number.&lt;/p&gt;

&lt;p&gt;The underlying issue is that ad hoc converters are usually built against a handful of example documents rather than the actual schema, so they only handle the shapes the author happened to think of.&lt;/p&gt;

&lt;p&gt;Common Mistakes&lt;/p&gt;

&lt;p&gt;A few patterns account for most of the pain:&lt;/p&gt;

&lt;p&gt;Losing attribute vs. element distinction. If Alice and 42Alice both become {"user": {"id": 42, "text": "Alice"}}, you can no longer round-trip back to the original XML shape, which matters if the other system still expects it.&lt;br&gt;
Silently collapsing repeated elements. A single  becomes a JSON string or object; two or more  elements should become an array — but only if the converter is consistent about it regardless of count. Inconsistent handling here is one of the most common sources of "works on this file, breaks on that one" bugs.&lt;br&gt;
Ignoring namespaces. Dropping namespace prefixes might look like a cleanup, but if two elements from different namespaces share a local name, you've just merged two unrelated fields.&lt;br&gt;
Type coercion surprises. XML has no types — everything is text until you say otherwise. JSON converters often guess: a leading zero on a string like "0042" might turn into 42, quietly corrupting IDs or zip codes.&lt;br&gt;
No round-trip testing. Converting XML → JSON → XML and diffing the result against the original is the cheapest way to catch all of the above, and it's skipped more often than not.&lt;br&gt;
A Better Approach&lt;/p&gt;

&lt;p&gt;The fix isn't to write a smarter one-off script — it's to stop treating this as a one-off. A few practical habits:&lt;/p&gt;

&lt;p&gt;Decide your attribute convention up front (prefix, sub-object, or merge) and apply it consistently, then document it so the next person doesn't reinvent it.&lt;br&gt;
Force arrays for elements that can repeat, even when a given document only has one instance, so your downstream code doesn't have to special-case "is this a string or a list."&lt;br&gt;
Preserve types explicitly where XML schema (or just convention) implies them, rather than letting a generic parser guess.&lt;br&gt;
Round-trip test a representative sample of real documents, not just your happy-path example.&lt;br&gt;
Use a conversion layer you don't have to maintain yourself for anything beyond a trivial, stable schema. It's rarely a good use of engineering time to hand-roll and maintain an XML/JSON converter for years.&lt;br&gt;
How ZenithConvert Helps&lt;/p&gt;

&lt;p&gt;This is the exact class of problem ZenithConvert is built for — an AI-powered format converter that handles JSON, XML, YAML, CSV, and close to 100 format pairs total, including the edge cases above (attributes, repeated elements, type ambiguity) instead of leaving you to special-case them. For one-off conversions there's a free online converter; for pipelines and CI, the REST API (with JS and Python SDKs) lets you drop conversion into a build step instead of maintaining a bespoke script. Guest use is capped at 3 conversions/day, a free account gets 5/day, and the API is available on paid plans for teams doing this at volume.&lt;/p&gt;

&lt;p&gt;Conclusion&lt;/p&gt;

&lt;p&gt;JSON-to-XML (and back) migrations look like a formatting exercise until you hit the handful of structural differences that don't map cleanly — attributes, repeated elements, namespaces, and types. Most of the bugs teams hit in production trace back to a converter that made an implicit, inconsistent choice about one of these. Deciding those conventions deliberately, testing round-trips, and offloading the actual conversion logic to a tool built for it will save more debugging time than it costs to set up.&lt;/p&gt;

&lt;p&gt;CTA: If you're maintaining a homegrown XML/JSON converter and want to see how it handles your actual documents, give ZenithConvert a spin — free tier included.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Booking Variable Import Duty as a First-Class Ledger Line (Not a COGS Fudge Factor)</title>
      <dc:creator>Badreddine Oussaih</dc:creator>
      <pubDate>Wed, 22 Jul 2026 08:56:32 +0000</pubDate>
      <link>https://dev.to/badr0h/booking-variable-import-duty-as-a-first-class-ledger-line-not-a-cogs-fudge-factor-2jb4</link>
      <guid>https://dev.to/badr0h/booking-variable-import-duty-as-a-first-class-ledger-line-not-a-cogs-fudge-factor-2jb4</guid>
      <description>&lt;p&gt;As of July 1, 2026, the EU removed its €150 duty-free threshold for low-value&lt;br&gt;
parcel imports. It's now replaced with a flat €3 customs duty &lt;em&gt;per HS code&lt;/em&gt;&lt;br&gt;
(tariff category) present in a shipment — a provisional rule running until&lt;br&gt;
the EU's full customs reform lands in mid-2028.&lt;/p&gt;

&lt;p&gt;That "per HS code, not per parcel" detail is the part that breaks naive&lt;br&gt;
ledger models. A single order with a T-shirt (HS 6109) and a leather wallet&lt;br&gt;
(HS 4202) now owes €6 in duty, not €3 — and that number isn't known until&lt;br&gt;
you've resolved every line item's tariff classification.&lt;/p&gt;

&lt;p&gt;I ran into this building the journaling engine for Comptyx, a cross-border&lt;br&gt;
accounting tool, and it's a decent case study in why "duty" can't be a flat&lt;br&gt;
percentage bolted onto COGS.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a flat rate breaks
&lt;/h2&gt;

&lt;p&gt;Most simple ledger implementations model import cost as:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;landed_cost = sale_price * duty_rate_estimate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That's fine when duty is genuinely proportional to value. It's wrong the&lt;br&gt;
moment duty is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;per tariff category, not per order value&lt;/li&gt;
&lt;li&gt;dependent on how line items are split across HS codes&lt;/li&gt;
&lt;li&gt;subject to change mid-cycle (the EU is separately floating a possible
€2/HS-code handling fee later this year — unconfirmed, but the schema
needs to tolerate a new fee type without a migration)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the model needs a &lt;code&gt;duty_line&lt;/code&gt; entity distinct from &lt;code&gt;order_line&lt;/code&gt;, keyed&lt;br&gt;
on HS code, not on order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema sketch
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CREATE TABLE duty_line (
    id              UUID PRIMARY KEY,
    order_id        UUID NOT NULL REFERENCES sales_order(id),
    hs_code         VARCHAR(10) NOT NULL,
    duty_amount     NUMERIC(12,2) NOT NULL,
    duty_currency   CHAR(3) NOT NULL,
    fee_type        VARCHAR(20) NOT NULL, -- 'CUSTOMS_DUTY', 'HANDLING_FEE'
    effective_date  DATE NOT NULL,
    idempotency_key VARCHAR(64) UNIQUE NOT NULL
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;code&gt;fee_type&lt;/code&gt; is deliberately open-ended — when/if the handling fee gets&lt;br&gt;
confirmed, it's a new row type, not a schema change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotent writes matter here more than usual
&lt;/h2&gt;

&lt;p&gt;Duty gets calculated by a downstream customs/carrier system and often&lt;br&gt;
arrives asynchronously, sometimes revised (carriers occasionally recompute&lt;br&gt;
after physical inspection). Two failure modes to guard against:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Duplicate duty postings if the carrier webhook retries&lt;/li&gt;
&lt;li&gt;Silent overwrites if a revision arrives and you don't reconcile against
the original&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The idempotency_key (built from order_id + hs_code + carrier_reference)&lt;br&gt;
handles (1). For (2), we don't overwrite — we post a reversing entry and a&lt;br&gt;
new line, so the ledger keeps a full audit trail of the correction:&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@Transactional&lt;br&gt;
public void postDuty(DutyLineRequest req) {&lt;br&gt;
    if (duttyLineRepo.existsByIdempotencyKey(req.idempotencyKey())) {&lt;br&gt;
        return; // already posted, no-op&lt;br&gt;
    }&lt;br&gt;
    if (req.isRevision()) {&lt;br&gt;
        ledgerService.reverse(req.originalLineId());&lt;br&gt;
    }&lt;br&gt;
    duttyLineRepo.save(DutyLine.from(req));&lt;br&gt;
    ledgerService.postJournalEntry(&lt;br&gt;
        JournalEntry.debit(req.dutyAmount(), req.dutyCurrency(), "import_duty_expense")&lt;br&gt;
                    .credit(req.dutyAmount(), req.dutyCurrency(), "customs_payable")&lt;br&gt;
    );&lt;br&gt;
}&lt;br&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  Multi-currency rounding, briefly&lt;br&gt;
&lt;/h2&gt;

&lt;p&gt;Duty is typically assessed in EUR regardless of the sale currency. If your&lt;br&gt;
base ledger currency isn't EUR, that's a second conversion on top of the&lt;br&gt;
sale conversion — and now you have two independently rounded amounts that&lt;br&gt;
need to reconcile to the transaction total. We store both the original&lt;br&gt;
duty currency amount and the converted ledger-currency amount on the same&lt;br&gt;
row, rather than converting once and discarding the source value — makes&lt;br&gt;
audits and rate-dispute resolution much less painful later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Regulatory changes like this are a good stress test for whether your&lt;br&gt;
ledger models cost as a static rate or as a first-class, independently&lt;br&gt;
sourced line item. The latter is more schema work up front, but it's the&lt;br&gt;
only version that survives the next rule change without a rewrite.&lt;/p&gt;

&lt;p&gt;Built this into Comptyx (&lt;a href="https://www.comptyx.com/" rel="noopener noreferrer"&gt;https://www.comptyx.com/&lt;/a&gt;) if you want to see the&lt;br&gt;
approach in a live cross-border accounting product — but the pattern above&lt;br&gt;
applies regardless of what you're building it in.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>fintech</category>
      <category>software</category>
    </item>
  </channel>
</rss>
