I have an application that handles products and sales in different currencies. So every row in the same table in the database can store prices in different currencies. How to correctly do that?
The most straightforward way is to define a numeric price_amount column and varchar price_currency oolumn, but I feel that two technically independent columns for essentially single value (price) is wrong. Like physical measurements are meaningless numbers without units, amount of money is also meaningless without their units–currency.
In my opinion money should be a single value containing both amount and currency within itself.
I started searching and got surprised a bit that there are no ready solutions or good articles in search results. There is pg-currency extension that does what I want, but it was abandoned almost 10 years ago.
I created the following composite datatype as a starting point:
CREATE TYPE true_money AS (
currency varchar,
amount numeric
);
And then starting to write supporting things for it: validations, arithmetic, aggregates… And realized that this rabbit hole is truly deep.
All my current (partial) results over this composite type can be found here for reference: https://gist.github.com/Envek/780b917e72a86c123776ee763b8dd986?fbclid=IwAR2GxGUVPg5FtN3SSPhQv2uFA7oPNNjbZeTYWRix-ZijYaJFRec15chWLA8#file-true_money-sql
And now I can do following things:
INSERT INTO "products" ("title", "price") VALUES ('Гравицапа', ('RUB',100500));
INSERT INTO "products" ("title", "price") VALUES ('Эцих с гвоздями', ('RUB',12100.42));
INSERT INTO "products" ("title", "price") VALUES ('Gravizapa', ('USD',19999.99));
-- You can access its parts if you need to extract them or do filtering or grouping
SELECT SUM(price) FROM test_monetaries WHERE (price).currency = 'RUB';
-- (RUB,112600.42)
-- And if you forget filtering/grouping then custom types can save you from nonsense results
SELECT SUM(price) FROM test_monetaries;
ERROR: (USD,19999.99) can not be added to (RUB,112600.42) - currencies do not match
Is it a correct approach? How to do it right?
A bit of context: in our app, users (sellers) can manage their stock (products) in any currency they want (e.g., USD, EUR, JPY, RUB, whatever). The app will convert currencies and publish products in local sites (like British or Australian). Buyers will also buy these goods in their local currency (GBP, AUD, etc.) that will eventually be converted to seller currency and paid to them (except fees). So in many places in the application, almost any supported currency can appear. And finally, the seller should be able to change their currency and all products should be converted to new currency in batches (single update within transaction can't be used by some reasons). So we can't say “keep only numeric values in the products table and JOIN with the sellers table to get currency” (which is anti-pattern per se, I believe).