Lesson 76Reporting and closeIntermediate

Constructing the income statement

Revenue, cost of revenue, opex, EBITDA, net income, all from your ledger.

By Solomon Ajayi · Free to read, no signup

Every accounting period (monthly, quarterly), your CFO needs an income statement: a summary of revenue, costs, and profit over the period. The good news: it's literally a SQL query over your journal lines, grouped by account type, filtered by posting date. The bad news: most fintechs have a chart of accounts that's grown organically, with revenue scattered across 40 accounts and expenses across 80. This lesson uses a tightly-curated CoA to show how a clean income statement falls out of well-named accounts. We post one revenue + one COGS + one opex entry so the structure is visible.

The income statement answers one question: did you make money over a period of time. It is not a balance, it is a flow, summed between two dates. Revenue at the top, then the costs that directly produced it (cost of revenue), then the operating costs that keep the business running, and what falls out the bottom is net income. The whole report is a grouping of your income and expense accounts over a date range, nothing more exotic than that.

Two ideas in this lesson make the report honest. The first is ordering: revenue minus cost of revenue gives gross profit, and that 80 percent gross margin tells you something different from net margin. The second is the matching principle. The salary entry accrues an expense into this period even though the cash leaves next period, because the expense belongs to the period that earned the revenue, not the period the cash happens to move. That is why the salary line debits an expense and credits a payable rather than the bank.

What makes this a one-line query instead of a three-day archaeology project is account naming. Revenue grouped by source, expenses grouped by function, and a flat account type that distinguishes income from expense. The accounts in this lesson are deliberately curated so the report falls out cleanly. The discipline is resisting the urge to mint a fresh account for every nuance, because every stray account is another row the report has to find and classify.

Worked example, step by step

Revenue line: ₦2M of transaction fees this period

Aggregate of many smaller revenue entries posted throughout the period. We show one summary entry for clarity.

Period transaction fee revenue
AccountDebitCredit
Bank Account (1200)₦2,000,000.00
Revenue: Transaction Fees (4000)₦2,000,000.00

Bank Account UP ₦2,000,000 (debit). Revenue: Transaction Fees UP ₦2,000,000 (credit). This account will roll into the 'Revenue: Transaction Fees' line of the income statement.

COGS: ₦400K in processor costs this period

Direct cost of producing the revenue (interchange, scheme fees, etc.). Sits on the COGS line of the income statement, between revenue and gross profit.

Period processor costs
AccountDebitCredit
COGS: Processor Costs (5000)₦400,000.00
Bank Account (1200)₦400,000.00

Bank Account DOWN ₦400,000 (credit, you paid the processors). COGS: Processor Costs UP ₦400,000 (debit, expense). Gross profit for the period = ₦2,000,000 - ₦400,000 = ₦1,600,000 (an 80% gross margin, typical for fintech).

Opex: ₦500K accrued salaries (recognised even if not yet paid)

Salaries earned this period but paid next period. Under accrual accounting, the expense lands in THIS period, that's the matching principle. The accrued liability sits on the balance sheet until cash actually goes out.

Accrue period salaries ₦500,000
AccountDebitCredit
Opex: Salaries (5100)₦500,000.00
Accrued Salaries Payable (2400)₦500,000.00

Opex: Salaries UP ₦500,000 (debit, expense). Accrued Salaries Payable UP ₦500,000 (credit, liability). Period net income = ₦1,600,000 - ₦500,000 = ₦1,100,000 (rough, we're missing marketing, but you see the shape).

Takeaway

The income statement is a SQL query: `SELECT account_code, SUM(credit - debit) FROM journal_line WHERE posted_at BETWEEN X AND Y AND account_type IN ('income','expense') GROUP BY account_code, account_type ORDER BY ...`. The DISCIPLINE that makes this work is account naming: revenue accounts grouped by SOURCE (transaction fees, interest, FX spread), expense accounts grouped by FUNCTION (COGS, Opex-Salaries, Opex-Marketing, Opex-Tech). Resist the urge to add a new account for every nuance, a clean CoA is the foundation of clean reporting. Your accountant will thank you; your auditor will move faster; your CFO can answer 'how much did we earn last quarter' in 10 seconds, not 3 days.

The code behind it

One grouped query turns the general ledger into a full income statement for a period.

-- Income statement for a period, straight from the ledger.
-- Convention: for income/expense, balance = SUM(credit - debit),
-- so revenue lands positive and expenses land negative. We just add them up.
WITH category AS (
  SELECT
    CASE
      WHEN a.code LIKE '4%' THEN 'revenue'   -- 4000 Transaction Fees, 4100 Interest
      WHEN a.code LIKE '50%' THEN 'cogs'     -- 5000 Processor Costs
      WHEN a.code LIKE '51%' OR a.code LIKE '52%' THEN 'opex' -- 5100 Salaries, 5200 Marketing
    END AS category,
    SUM(jl.credit - jl.debit) AS amount  -- naira kobo as integer minor units
  FROM journal_line jl
  JOIN account a ON a.code = jl.account_code
  WHERE a.type IN ('income', 'expense')
    AND jl.posted_at >= DATE '2026-06-01'   -- period start (inclusive)
    AND jl.posted_at <  DATE '2026-07-01'   -- next period start (exclusive)
  GROUP BY 1
)
SELECT
  SUM(amount) FILTER (WHERE category = 'revenue')                  AS revenue,
  SUM(amount) FILTER (WHERE category IN ('revenue','cogs'))        AS gross_profit,
  SUM(amount) FILTER (WHERE category IN ('revenue','cogs','opex')) AS operating_income,
  SUM(amount)                                                      AS net_income
FROM category;
-- revenue 2,000,000 | gross_profit 1,600,000 | operating_income & net_income 1,100,000

Practice this on a real ledger

Reading is half of it. Open this lesson in the lab to post the entries yourself against a real Postgres-backed double-entry ledger, with the validation on. Free, your sandbox is yours.

More in this section

Search lessons

Type to find any of the 85 lessons. Press Enter to open.