[HN Gopher] Things I learned while developing a billing system
___________________________________________________________________
Things I learned while developing a billing system
Author : arnon
Score : 147 points
Date : 2021-04-05 17:03 UTC (5 hours ago)
(HTM) web link (arnon.dk)
(TXT) w3m dump (arnon.dk)
| loop0 wrote:
| I went through the same issues 6 years ago, had to create the
| entire billing system from scratch, and there are so many corner
| cases I had to deal with, specially with plan changes on
| recurring subscriptions, had to deal with world wide timezones
| for the invoice creations and lots of other stuff. I just wish
| there was a pluggable, robust and flexible solution to use
| instead of reinventing the wheel.
| epa wrote:
| All of these features are fairly basic and routine and should
| have been part of scoping. The issue is that engineers rarely
| speak to finance teams to gather input which leads to these
| 'surprises'. The article is equivalent to "how hard could it be
| to win my lawsuit, i researched the law".
| encoderer wrote:
| Ok, genius, you go write a blog post then.
| yellowyacht wrote:
| > Ok, genius, you go write a blog post then.
|
| That was unnecessarily antagonistic
| encoderer wrote:
| Totally disagree. I came here for commentary on the blog
| post not a cliche HackerNews trope. When I replied, his was
| the top comment. Honestly, guys like that need a nudge
| every now and again to train their filter. A downvote just
| isn't enough. Welcome to the community.
| spelunker wrote:
| To be fair, this blog is apparently written by a product
| manager, who presumably discovered these features by doing
| research. And probably asking finance teams.
| arnon wrote:
| Accurate
| [deleted]
| kube-system wrote:
| > Store money including the smallest possible subdivisions.
|
| > For example:
|
| > $100 US may be stored as 10000
|
| Eh, the US dollar is often subdivided further than that.
|
| Here's four examples, in different industries:
|
| http://cdn.radioiowa.com/wp-content/uploads/2011/12/gas-pump...
|
| https://aws.amazon.com/s3/pricing/
|
| https://finance.yahoo.com/u/yahoo-finance/watchlists/most-ac...
|
| https://www.digikey.com/en/products/detail/microchip-technol...
| kbenson wrote:
| Those are unit prices or interval prices. Any transaction will
| almost definitely be in an amount of dollars and centers if
| dealing in US, rounding to a whole cent based on the total
| units or interval.
|
| For example, if you're paying #3.089 a gallon at the pump, and
| you pump exactly one gallon, you don't pay $3.089, you'll pay
| $3.09, and neither your account or the account you're paying
| into will need to know or care about the tenth of a cent
| difference, because our monetary system doesn't really deal
| with denominations that small in transactions.
| deckard1 wrote:
| yeah but this is where you really have to be careful where
| you do your rounding. 3.089 * 10,000 = 30890
| 3.09 * 10,000 = 30900
|
| A one cent rounding added up to $10 difference. It's easy to
| screw this up in code, sending values to databases or across
| APIs, etc.
|
| Also reminds me of the plot of Office Space. Which is a bit
| humorous since they screwed up their own scamming scheme in
| similar fashion.
|
| https://www.youtube.com/watch?v=yZjCQ3T5yXo
| mr-wendel wrote:
| My personal favorite: how long is a month?
|
| - _An even 30 days_ = Easy to explain and do math with. Adding
| /subtracting 1 month can leave you in the same month or skip
| February entirely! Anniversary dates bounce around from month-to-
| month. Maps poorly to yearly calculations.
|
| - _However many days >this< month has_ = Harder to explain and do
| math with. Months without 31 days can get skipped when
| adding/subtracting 1 month. Recurring events get pushed away from
| the end of the month to cluster around the beginning of the
| month. It does result in a steady anniversary date for edge
| cases.
|
| - _Closest numeric day, but prev /next month_ (e.g. Jan 31 -> Feb
| 28): Easy _and_ hard to explain and do math with. It 's great for
| ensuring events only ever happen once per calendar month, but
| adds extra ambiguity (e.g. Jan 31 > Feb 28 > March 30? ... or
| March 28th?)
|
| - _30.4..._ = Just no... except for the few times this is right
| and you need consistency to avoid unfairly comparing 28 days vs
| 31 days.
|
| The consequence is some very surprising things the first time you
| see them. Some billing systems simply avoid doing work past the
| 28th of each month (either doing it a bit early or a bit late).
| Some just embrace the weirdness (whichever you flavor you pick)
| and you get used to the quirks (e.g. end-of-month lulls and
| start-of-month spikes).
| sk5t wrote:
| I'll unwaveringly go with whatever the hardass valid-date-
| enforcing mechanisms in java.time and PostgreSQL enforce.
|
| Add a month to the epoch time? Can't do it because that doesn't
| adequate indicate what day we think it is. "select
| '2021-01-30'::date + make_interval(months := 1)" says Feb 28?
| Done!
| throwawayboise wrote:
| Absolutely. Go with the date/time/interval functions that
| your database or language libraries provide. Do not try to
| reinvent this, you will get it wrong 10 times before you get
| it right.
| arnon wrote:
| My go-to for everything is WWPD - "What would Postgres do?"
| tzs wrote:
| If I ever get to redesign the billing system at work, I'm going
| to go with fixed day of the month billing. We currently do 30
| day billing, and most customers are fine with that, but there
| are some who need their bill to not come the same week as their
| rent is due, or not come until after they receive their monthly
| pension payment, or something like that. Right now we have to
| have special handling for those people.
|
| For handling the different lengths between the months, I've
| considered a few approaches.
|
| 1. If re-bill is supposed to be the Nth of each month, that
| simply means N-1 days after the 1st. If that falls into another
| calendar month, fine. So if someone asks to be billed on the
| 30th of each month, they will be billed on Jan 30th for
| January, Mar 2nd for February, Mar 30 for March, and so on. I'd
| make sure the receipt explicitly says "Re-bill for January",
| "Re-bill for February", etc., so that bill the see on Mar 2nd
| hopefully will not confuse them, nor will seeing another bill
| later that month.
|
| 2. If re-bill is supposed to be on the 29th-31st, for February
| it takes place on the 28th (29th in leap years). For other
| months, if re-bill is supposed to be on 31st, it takes place on
| the 30th in April, June, September, and November.
|
| 3. If you initially order on day 1-21 of the month, your re-
| billing is the same day ever month. If you initially order on
| day 22+, your re-billing is on the same number of days before
| the end of the month as was your initial order. For example,
| someone who orders on May 30th would re-bill on the 30th in 31
| day months, the 29th in 30 days months, and the 27th in
| February (28th in leap years). If you ask for a change in fixed
| billing date, you can do so either as Nth day of the month or
| Mth day from the end of the month, but N or M must be <= 28.
| PeterisP wrote:
| For additional fun, consider how this would work with the
| added constraint of needing the date be a business day, so
| you need to take into account how weekends (which vary across
| the world, by the way; in a bunch of countries sunday is a
| business day but friday is off) and banking holidays in every
| locale in which you do business.
| notimpotent wrote:
| First time learning that weekend days vary by country. I
| looked up [1] some of the more curious ones.
|
| Brunei weekends are Friday and Sunday. Those poor folks
| work Mon-Thurs, and then go back to work Saturday!
|
| [1] https://en.wikipedia.org/wiki/Workweek_and_weekend
| thaumasiotes wrote:
| For reference, in China there are a bunch of legally
| mandated holidays that convert the weekend days around
| them into normal working days. (The conversion is not
| legally mandated; think of it as a response to laws that
| generate many more holidays than the economy supports.)
| thaumasiotes wrote:
| > For handling the different lengths between the months, I've
| considered a few approaches.
|
| > 1. If re-bill is supposed to be the Nth of each month, that
| simply means N-1 days after the 1st.
|
| I have a credit card that takes the much simpler approach
| "Your payment due date is the Nth of each month. N can be any
| value from 1 to 24."
|
| This seems to basically match your approach 3.
|
| If you ask how I think you should bill ( :p ), do it on a
| fixed day and don't try to do rebilling at a fixed interval.
| If the day somebody buys your thing is unsuitable for fixed-
| date billing, the answer is prorating their first period, not
| interval billing.
| arnon wrote:
| > If I ever get to redesign the billing system at work, I'm
| going to go with fixed day of the month billing.
|
| This may have unintended consequences. Some examples we faced
| around this:
|
| 1. Stress on our APIs when 15,000 invoices are created in the
| span of a couple of minutes. We had to build more queueing
| mechanisms around this. Some services just can't handle more
| than 100 calls a second.
|
| 2. Getting rate limited by MasterCard
|
| 3. Some payment providers thinking we're brute-forcing them
| virtue3 wrote:
| Aren't all of these handled by a queue and some reasonable
| sense of going through it slowly? (I can just say that on
| the internet, I know designing it that way is not exactly
| trivial).
| arnon wrote:
| Not all systems have sane defaults or queues built-in.
|
| I'm just saying this needs consideration when you reach a
| certain scale.
| mr-wendel wrote:
| If you ever get to... GET TO? You must love pain! Actually, I
| wonder what business you're coding for to have those be
| important criteria and wish you all the best of luck!
|
| In general, however:
|
| Don't fall victim to one of the classic dev blunders -- the
| most famous of which is "don't roll your own crypto" -- but
| only slightly less well-known is this: "Never write your own
| billing logic when business is on the line!"
| paulclinger wrote:
| Maybe if the date is "close" to the end of the month (as in
| fewer than X days from the end of the month), then count in the
| days left, so Jan 31st (0 days left) becomes Feb 28th and then
| to Mar 31st and so on.
| vidanay wrote:
| This is why I've never understood why payroll is commonly on
| the 15th and last days of the month. Why not 1st and 15th?
| Those dates never change.
| pc86 wrote:
| In my experience 1/15 is no less common than 15/L or every
| two weeks.
| gergely wrote:
| These use cases are also "fun" if you add timezones as well
| into the mix!
| deckard1 wrote:
| _shudder_
|
| I can only imagine needing to toss in standard/daylight
| timezone switch in there. Billing code really is the pinnacle
| of developer pain. It mixes the arbitrariness of special case
| business and customer rules with the absolute horror of time
| and date math.
| pc86 wrote:
| Not only that but in T-SQL, `AT TIME ZONE` takes DST into
| account but not in the names. So something like:
| SELECT CreatedDate AT TIME ZONE 'UTC' AT TIME ZONE 'Eastern
| Standard Time' FROM MyTable
|
| would in fact return `CreatedDate at Easter _Daylight_ Time
| if executed today.
| sorbits wrote:
| I would base anything dealing with money on a double entry
| accounting system.
|
| The issues that arise are then a question about how to translate
| them to journal entries, which will require accounting
| experience, and maybe your chart of accounts needs to be revised,
| but the core system should be fairly stable, and voiding an
| invoice by issuing a credit note comes automatically, as you're
| working with an append-only ledger.
|
| The issues about prepaid plans should also be handled, as
| payments for services not rendered yet should not be recognized
| as income, but instead kept as a liability: The OP mentions their
| system is used by 15,000 customers, so I would give each customer
| their own account (in the chart of accounts).
|
| The main technical issue is what datatype to use for money, the
| rest are problems solved by following general accounting
| principles, though I fear that a lot of programmers out there are
| reinventing the wheel (in suboptimal ways).
| throwawayboise wrote:
| > payments for services not rendered yet should not be
| recognized as income, but instead kept as a liability
|
| This entirely depends on whether you are operating on a cash or
| accrual basis. Both approach are valid (at least in the USA)
| and cash basis is often used by small businesses.
| sorbits wrote:
| I use the word "should" as per RFC 2119, i.e. recommended (as
| opposed to "must" = required).
|
| Though the IRS does limit the types of businesses that can do
| cash-basis accounting, it would be businesses without
| inventory, and who does not offer credit to their customers,
| e.g. a hairdresser would probably use cash-basis accounting,
| but more complicated businesses would not, certainly not a
| business that needs its own billing system :)
| gerikson wrote:
| Obligatory: https://blog.plover.com/prog/Moonpig.html
| arnon wrote:
| Thanks for that! I haven't seen this...
|
| I'm glad to see he also has the same thought about floating
| points. Also:
|
| > Happily, Moonpig did not have to deal with multiple
| currencies. That would have added tremendous complexity to the
| financial calculations, and I am not confident that Rik and I
| could have gotten it right in the time available.
|
| This is one of the things we deal with which is quite
| challenging.
| kjrose wrote:
| Having worked on billing and accounting systems for decades now.
| I am constantly amazed by all of the edge cases and situations
| that have to be handled to ensure that every bill is correct and
| nothing is missed or misbilled. Especially since the oil firms
| out here will reject an invoice if it is off by as little as a
| penny.
|
| At first it seems like it's so easy and straightforward but when
| you are looking at thousands if not millions of bills a year and
| the myriad methods you need to compensate for to ensure its
| accepted by large firms.... well. I can say nothing surprises me
| anymore. Including hardware level errors causing an issue no one
| expected with a final number.
| cosmodisk wrote:
| The principles behind it all are pretty simple and well
| established. What makes it complicated are the people and
| poorly designed systems.
| throwawayboise wrote:
| Right -- accounting seems simple. It's just debits and credits
| and adding it all up. Until you get into it, and you understand
| why "Accounting" can easily be a department of dozens or
| hundreds of people.
| kjrose wrote:
| There's a reason to separate your accounts receivable from
| payables. To separate invoicing from receipts.
|
| And so on and so on. You don't realize it until you suddenly
| realize why it was such a bad idea to have one point of
| failure for everything.
| ehutch79 wrote:
| There are absolutely time when you need to deal with amounts
| smaller than the smallest division of a currency.
|
| For example: 1 ea @ $0.00589 Normally this is when ordering in
| the thousands or millions of small things, but you need to record
| that fractional size.
| Kiro wrote:
| Yeah, this immediately broke my idea of only dealing with cents
| and integers. All of a sudden I wanted to price something
| $0.001 and it all came crashing down.
| canada_dry wrote:
| Here's a lesson (I can laugh about now that I'm retired): one of
| the first billing systems I ever wrote (in the 90's) had invoices
| with a mask of '$ZZ,ZZ9.99' based on interviews with staff. About
| a year later I get an urgent call from the owner. It seems a
| client was under-billed about $100K dollars due to truncated
| invoice.
|
| Subsequently, whenever interviewing clients for requirements I'd
| mention this and it usually resulted in padding their specs.
| pfranz wrote:
| One edge-case I've noticed with Apple's subscription billing and
| heard other people talk about when implementing billing systems
| is changing currencies/countries. Apple, in line with most
| people's expectations, when you cancel a subscription it doesn't
| cancel immediately and prorate a refund. It just stops future
| billing and lets you serve out your remaining cycle (gyms and
| other meatspace places do this). The problem is you then have to
| wait until your subscription expires to transfer your account to
| a new country/currency.
| breischl wrote:
| My favorite: when do you do rounding?
|
| For instance, with proration. Take each line item, prorate it,
| round it, then add them all together. Totally reasonable.
|
| Now add all the line items together, prorate the total, and round
| it. Also reasonable, but there's a decent chance the number is
| different by a few pennies because of rounding differences. If
| you chained more calculations the differences would compound.
|
| Whichever way you choose, somebody is going to whip out their
| calculator and tell you that you did it wrong (but only when they
| come out better the other way).
|
| This applies anywhere you're doing multiplication or division.
| Discounts, proration, taxes, "cashback", "store credit dividend",
| whatever.
| ianmcgowan wrote:
| In my world, we keep a running total of the rounded prorated
| amounts, and then the last item (n) becomes (total - total up
| to n-1) to make sure amounts match. It can still be a problem
| if the last value is very small however.
| unixhero wrote:
| Billing is very hard. That is why systems like Geneva is used.
| mmcconnell1618 wrote:
| Another fun use case is when a customer is charged for their
| invoice via credit card and then 60 days later, you get a
| chargeback from the bank. Now you have to unwind the previous
| invoices and entitlement systems to figure out if you should
| fight the chargeback, cancel the subscription or some other non-
| standard process.
| berkes wrote:
| In my last financial product we added 'bitcoin' and 'festival
| tokens' as currencies in the backend. At first for fun.
|
| But we found out the client devs (web, mobile apps) had severe
| difficulties and wanted it removed. Turned out they all
| implemented currencies wrong. Either with hardcoded decimals or
| with localised in and outputs that would break if a client
| changed their locales.
|
| So now, my favorite best practice for any financial data handling
| is that: ensure your system can handle Bitcoin (8 decimal places)
| and festival tokens (missing currency symbol, zero decimals).
| Anywhere this leads to trouble is a red flag and will probably
| cause trouble later on. Now at least you are aware.
| michael1999 wrote:
| That's great advice. I'll add it to my list of round-tripping
| German, and Japanese text with emojis, and changing the
| timezone.
| arthurcolle wrote:
| What do you mean by round tripping German here?
|
| I think I get the issues with the other two, but curious
| about the first thing!
| dan-robertson wrote:
| Not sure about round tripping but perhaps the longer words
| might stress test webpage layouts.
| [deleted]
| [deleted]
| brixon wrote:
| "Whatever limitations you plan for, plan for how to bypass them
| too. This will happen."
|
| The world is not straightforward, allow an admin to fix/change
| anything and when they get tired of making some change then code
| that new path in the system. Working with or writing systems that
| do not allow admin overriding is painful.
| bidirectional wrote:
| > A common wisdom in database design is "never using floating-
| point numbers for money"
|
| Common, and in my experience totally wrong. It's the most
| pervasive cargo culting I've experienced amongst developers,
| where people with 0 experience with financial applications will
| recoil if you argue against it. In my time developing
| applications for front office at an investment bank, floating
| point often works best.
|
| Of course in your case though, for a billing system, the method
| you describe is obviously the right one.
| bxparks wrote:
| You are getting a lot of down-votes, but you are correct, and I
| gave you some upvotes, at least one.
|
| If the application is doing financial modeling and estimations,
| where only 1-3 decimal place of accuracy is needed, then
| floating point is the right choice. It greatly simplifies the
| app.
|
| If the application is doing accounting, payments and billing,
| where people expect accuracy to the penny or more (e.g. to
| 1/10000 of a penny), then it needs to use a Decimal type.
| mamcx wrote:
| Floating point is bad as null are, and is defended by the same
| apologist of null: "I don't make mistakes... so is good!".
|
| Floating point ARE a major source of errors across everyone
| that use them. ARE infectious. ARE semantically wrong. ARE not
| made for financial calculation.
|
| ARE WRONG.
|
| Period. Just because under a lot of discipline (or luck, or
| just "assume" is working but nobody have checked, or work
| before but how knows if today?) not make it a good choice for
| financial/money.
|
| Is the same error when people think old String types can be
| used in the unicode world, instead of have a proper type for
| that.
|
| Luck help a lot. But is not something to be proud about.
| cameronh90 wrote:
| Finance isn't just banking and accounting. A huge amount of
| finance is modelling, simulations, signal generation, etc.
| where being fast is often way more important than being
| completely 100% accurate. In financial modelling, errors from
| floating point are going to be insignificant compared to all
| the other assumptions you make in your models.
|
| I have worked in finance for my entire career, and everyone
| uses floating point arithmetic for dealing with money on the
| modelling side (sometimes $0.01 = 1.0, sometimes $1 = 1.0,
| depends on the institution/currency/convention/context, but
| we have to deal with fractional money anyway).
|
| They do NOT use it on the accounting/back office side. That
| would be a spectacularly bad idea. Those systems are designed
| for accuracy to the penny.
| bidirectional wrote:
| Well no, the key difference between null and floating point
| is that for all its flaws, floating point is _by far_ the
| fastest way of doing non-integer numerical computation. Null
| is just an ugly convenience hack of arguable merit, floats
| are fundamental.
|
| They're not a major source of error when you're implementing
| a model which is already inexact to a far larger degree than
| the problems caused by floats. Black-Scholes does not
| perfectly price an option, your bootstrapped curve is not a
| perfect predictor of market conditions in 28 years time.
| These are the problems faced in front-office finance, the
| error is already so far beyond 1 + 0.1 not perfectly matching
| 1.1 that it's not worth caring about. I've worked on
| applications where users just wanted to see numbers to the
| nearest 100k so they could model out a few trades they
| planned to make over the phone.
|
| When you're working on a retail banking app where you need to
| track customer's balances, or an accounting system, or
| anything like that, then sure, floating point would be
| malpractice. That is nothing like any of the applications
| I've worked on in the financial field.
| devwastaken wrote:
| How did you handle the properties of floating point? The reason
| people recoil at that more than likely has to do with lack of
| knowledge in how floats are wrangled to ensure they're
| accurate.
| stevesimmons wrote:
| Which properties?
|
| For front-office risk and pricing calcs, speed of calculation
| matters most. Hence IEEE754 float64.
|
| Fixed precision decimals really only matter for middle and
| back-office, for trade confirmation and settlement.
| tantalor wrote:
| What's the difference between front/middle/back office?
| arthurcolle wrote:
| In the context of a bulge bracket investment bank, it's
| basically like this:
|
| Front office - S&T (sales & trading), i.e. revenue
| generating activity. Usually includes any quants/quant
| developers actively working on things that make money
|
| Middle office - operations. handles settlements,
| confirms, and generally anything related to the post-
| trade flow that is "after the trade is booked"
|
| Back office - accounting, legal, engineering, IT support,
| HR. Anything that isn't a revenue center that also isn't
| even tangentially facing revenue generating operations
| bidirectional wrote:
| Which properties do you mean in particular? The main point is
| just that finance doesn't necessarily require perfect
| accuracy. It does when Bob sends Alice $1 and their account
| balances must line up perfectly, it doesn't when you're
| implementing models which already have greater error than
| floating point could imbue. Not to mention that decimals,
| integers and rationals cannot compute something as simple as
| compound interest without lack of accuracy, so they're not
| going to save you when you're implementing Black-Scholes.
| lamp987 wrote:
| >rationals cannot compute something as simple as compound
| interest without lack of accuracy
|
| Care to elaborate?
| bidirectional wrote:
| (Continiously) compound(ed) interest is e^(rate * time),
| e is not a rational number. Transcendental functions are
| commonly used in financial modelling, and once they
| appear, nothing short of a full-blown CAS will give you
| 100% accurate results (not that you should care, because
| your model is off by more than floating point error).
| gamache wrote:
| > finance doesn't necessarily require perfect accuracy
|
| Maybe finance doesn't, but billing does.
| bidirectional wrote:
| Yes, of course, I said that in my original comment. I'm
| arguing the advice as a panacea, not saying the OP was
| wrong to follow it.
| smallnamespace wrote:
| Yes, every time someone categorically declaims that 'floats and
| money should never mix', I question whether they've ever met a
| real accountant.
|
| Accountants frequently spend all day in Excel. Excel uses
| floats for all computations (doubles, to be precise) [1].
|
| Now, in all fairness, an accountant and developer's
| relationship to the numbers is rather different.
|
| For an accountant, the risk of floats blowing up is largely
| mitigated by the fact that they have a close, intimate
| relationship with the actual numbers.
|
| The responsible accountant should always deliver numbers they
| have personally reviewed, while a developer is usually
| automating a process, generating numbers that have yet to touch
| a human eye, so there is rather less room for error.
|
| Still, it's simply untrue that one should _never_ use floats
| for money. Many of the cases where floats would generate bad
| results are also problematic for simple alternatives. For
| example, fixnums are simply 'floats that can't float', so you
| need to be able to guarantee a fixed range ahead of time.
|
| Understanding basic numerical analysis is unavoidable to
| writing correct code.
|
| [1]
| https://en.wikipedia.org/wiki/Numeric_precision_in_Microsoft...
___________________________________________________________________
(page generated 2021-04-05 23:01 UTC)