sharia-compliant-finance
v2.0.0
Published
Islamic finance toolkit: Sharia-compliant BNPL and instalment financing (murabaha, ijara, tawarruq, diminishing musharakah), a zakat engine with nisab and hawl, a configurable Shariah screening rule engine, and riba/gharar analysis. Integer-precision mone
Maintainers
Readme
sharia-compliant-finance
Islamic finance primitives you can put in a real product: Sharia-compliant BNPL and instalment financing, a zakat engine that actually knows about nisab and hawl, a configurable Shariah screening rule engine, and riba/gharar analysis.
Zero runtime dependencies. TypeScript source, ships ESM + CJS + type declarations. Money is integer minor units, so schedules always reconcile.
npm install sharia-compliant-financeWhy v2 exists
v1 was eight functions built on hard-coded arrays. It answered questions with bare booleans, which meant you were told no without being told why, and it got several answers outright wrong:
| v1 behaviour | What v2 does |
| --- | --- |
| calculateZakat(20, 'cash') returned 0.50 | Nisab is tested first. Below the threshold, zakat is zero. |
| No holding-period check | Hawl is tracked as a lunar year, with the due date returned. |
| Every asset taxed at 2.5% of face value | Fourteen asset classes, each with its own rule. Livestock and crops have their own nisab and rates entirely. |
| Liabilities ignored | Deducted, with the rule varying by madhhab. |
| industry: "Liquor Distribution" passed as halal | Resolved by alias, regex, and NAICS/SIC/GICS code. Unknown input reports insufficient-data, never a silent pass. |
| A supermarket with 1% alcohol revenue was banned outright | Revenue-share tolerance, exactly as index screens work, plus a purification amount. |
| Riba meant interestRate > 0 | Also catches tenor-linked fees, late charges the lender keeps, compounding, repricing on default, floating benchmarks, and riba al-fadl / al-nasiah in commodity and currency exchange. |
| isGhararPresent needed you to have done the analysis already | Fourteen structural checks on subject matter, price, delivery, and contingencies, graded yasir vs fahish. |
| Loss shared by profit ratio | Profit by agreed ratio, loss strictly by capital — and in a mudarabah, entirely by the capital provider. |
| Float arithmetic | Integer minor units with largest-remainder allocation. |
Every v1 function still works. See MIGRATION.md.
Islamic BNPL in twenty lines
Murabaha is the structure behind almost every Islamic BNPL product: the financier buys the goods, takes ownership risk, then resells at a disclosed cost plus a fixed markup, payable in instalments.
import { createMurabaha, toDecimalString } from 'sharia-compliant-finance';
const plan = createMurabaha({
currency: 'AED',
assetCost: 3600, // what you paid for the item
markup: { type: 'percentage', value: 8 }, // fixed at contract, forever
installments: 4,
frequency: 'monthly',
contractDate: '2026-03-31',
possession: { // you must own it before you sell it
acquiredAt: '2026-03-28',
type: 'physical',
description: 'Smartphone, 256GB',
},
latePayment: { type: 'charity-penalty', ratePerAnnum: 5, gracePeriodDays: 7 },
rebatePolicy: { type: 'full-unearned-profit' },
});
toDecimalString(plan.sellingPrice); // '3888.00'
plan.installments.map(i => i.dueDate);
// ['2026-04-30', '2026-05-31', '2026-06-30', '2026-07-31'] ← month-end respected
plan.compliance.verdict; // 'compliant'
plan.disclosure.effectiveAnnualRate; // for regulatory disclosure only; nothing accruesThe instalments always sum to the selling price exactly. Rounding lands on the earliest payments, never as a mystery balance at the end.
What the schedule enforces
createMurabaha({ ...plan, possession: { acquiredAt: '2026-04-05' } });
// finding: murabaha.sold-before-ownership — violation
// "The financier acquired the asset after selling it. That is a sale of what
// one does not own."Early settlement (ibra')
The debt is a fixed price, so the customer owes the whole remaining balance whether they pay today or on the last due date. A discount is the financier waiving something, and the quote says so explicitly:
const quote = quoteEarlySettlement({ schedule: plan, installmentsPaid: 2 });
quote.grossOutstanding; // 1944.00 — what is actually owed
quote.unearnedProfit; // 144.00
quote.rebate; // 144.00 — granted, not deducted by right
quote.settlementAmount; // 1800.00Policies: none, full-unearned-profit, percentage-of-unearned, fixed, or
your own function. Setting contractuallyGuaranteed: true raises a finding,
because AAOIFI requires ibra' to stay discretionary while Bank Negara Malaysia
requires the opposite — the library records which regime you are following
rather than picking for you.
Late payment — the part conventional BNPL gets wrong
const charge = calculateLatePaymentCharge({
currency: 'AED',
policy: { type: 'charity-penalty', ratePerAnnum: 5, gracePeriodDays: 7 },
overdueAmount: 972,
dueDate: '2026-04-30',
asOf: '2026-05-30',
});
charge.amount; // the deterrent charge
charge.payableTo; // 'charity'
charge.recogniseAsIncome; // false — you may not book a cent of it
charge.outstandingDebtUnchanged; // true — the price cannot grow. Ever.The charge is simple, never compounded: a charge for sixty days is exactly
twice one for thirty. Use { type: 'actual-cost' } to recover documented
collection expense instead, which you may retain.
Rescheduling
rescheduleFinancing({ schedule: plan, installmentsPaid: 2, newInstallmentCount: 8 });
// Spreads the same total over more payments. previousTotalPayable === newTotalPayable.
rescheduleFinancing({ ...same, additionalMarkup: 50 });
// throws ShariaConstraintError — "an increase in exchange for time is riba al-jahiliyyah"Other structures
| Function | Use for |
| --- | --- |
| createMurabaha | Goods BNPL, asset finance, trade finance |
| createIjara | Leasing, including ijara muntahia bittamleek |
| createDiminishingMusharakah | Home finance, high-value co-ownership |
| createTawarruq | Cash financing — always warns; see below |
All four return the same FinancingSchedule shape, so one checkout component
renders any of them.
createIjara refuses to let you push major maintenance, asset takaful, or
total-loss risk onto the lessee — a lessor that carries no ownership risk is a
lender, and the rent is then interest.
createDiminishingMusharakah rejects a buy-back at original nominal value,
which would guarantee the financier's capital and make the whole thing a
secured loan.
createTawarruq always emits a warning and flags the organised form as a
violation, citing OIC Fiqh Academy resolution 179 (2009). It exists because
real products use it, not because it is recommended.
Zakat
Nisab is a weight of gold or silver, so it needs a live metal price. This package makes no network calls and ships no market data — you supply the prices, which also means the number is auditable against a quote you can point to.
import { calculateZakat } from 'sharia-compliant-finance';
const result = calculateZakat({
currency: 'PKR',
assets: [
{ type: 'cash', value: 500_000 },
{ type: 'gold', grams: 50, purityKarat: 22, personalUse: true },
{ type: 'receivable', value: 80_000, recoverability: 'doubtful' },
{ type: 'shares-passive', value: 300_000, zakatableAssetRatio: 0.42 },
{ type: 'rental-property', value: 9_000_000, incomeHeld: 120_000 },
{ type: 'personal-use', value: 2_500_000 }, // home — exempt
],
liabilities: [
{ label: 'Credit card', amount: 40_000, type: 'immediate' },
{ label: 'Home finance', amount: 6_000_000, type: 'long-term', monthlyInstalment: 45_000 },
],
metalPrices: { currency: 'PKR', goldPricePerGram: 21_000, silverPricePerGram: 260 },
hawlStartDate: '2025-02-01',
valuationDate: '2026-02-05',
madhhab: 'hanafi',
});
result.zakatDue; // Money
result.aboveNisab; // boolean
result.hawl; // { satisfied, dueDate, daysElapsed, daysRemaining }
result.assets; // every asset with included/excluded and the reason
result.findings; // warnings about assumptions the engine had to makeAsset classes
cash · bank-balance · receivable (good / doubtful / bad) · gold ·
silver · trade-goods · shares-active · shares-passive · crypto ·
pension · rental-property · fixed-asset · personal-use · other
Each carries its own rule. A rental building is exempt while the rent retained at year end is not; a passively held share is assessed on the issuer's zakatable assets, not the whole share price; plant and machinery are tools of trade and exempt.
Madhhab
The four schools genuinely differ on questions that change the number. Each profile is explicit and overridable:
calculateZakat({ ...input, madhhab: 'hanafi' }); // worn jewellery IS zakatable
calculateZakat({ ...input, madhhab: 'shafii' }); // worn jewellery is exempt| | Nisab | Worn jewellery | Debt deduction |
| --- | --- | --- | --- |
| Hanafi | silver | zakatable | all debts |
| Shafi'i | gold | exempt | due only |
| Maliki | gold | exempt | due only |
| Hanbali | gold | exempt | all debts |
| contemporary (default) | lower of the two | exempt | next 12 months |
Livestock and crops
Not everyone's wealth is a bank balance.
calculateLivestockZakat({ kind: 'camel', count: 50, freelyGrazing: true });
// due: [{ count: 1, grade: 'hiqqah', description: 'A three-year-old female camel.' }]
calculateAgricultureZakat({ harvestKg: 4000, irrigation: 'natural' });
// { rate: 0.10, zakatDueKg: 400 } ← 5% if artificially irrigated, 7.5% if mixedScreening
The rule engine that replaces v1's six-string array.
import { screen, createScreeningEngine, AAOIFI_POLICY } from 'sharia-compliant-finance';
screen({ industry: 'Liquor Distribution' }).verdict; // 'non-compliant'
screen({ industryCodes: [{ scheme: 'NAICS', code: '713210' }] }).verdict; // 'non-compliant'
screen({ industry: 'artisanal widget refurbishment' }).verdict; // 'insufficient-data'That last one is the point. v1 passed anything it did not recognise. This reports that it could not tell, and tells you to classify it.
Revenue tolerance, not blanket exclusion
screen({
name: 'Supermarket',
revenueBreakdown: [
{ activity: 'grocery', revenue: money(980_000, 'USD') },
{ activity: 'alcohol', revenue: money(20_000, 'USD') },
],
});
// verdict: 'requires-review'
// nonCompliantRevenueShare: 0.02, within the 5% tolerance
// → purificationRate tells you how much income to give awayFinancial ratios
Built-in policy packs: aaoifi, djim, sp-shariah, msci-islamic,
ftse-shariah, activity-only. They differ in both thresholds and
denominators, which is why they must be selectable rather than baked in.
const report = screen({ name: 'Example Plc', industry: 'software', financials }, { policy: AAOIFI_POLICY });
report.ratios;
// [{ ruleId: 'aaoifi.debt-to-market-cap', value: 0.5, threshold: 0.3, passed: false, ... }]Making it yours
const engine = createScreeningEngine({
policy: definePolicy({ id: 'house', label: 'Our board', activity: { revenueTolerance: 0 } }, AAOIFI_POLICY),
// Override a ruling, or add a category the taxonomy has never heard of
activities: [
{ id: 'tobacco', label: 'Tobacco', ruling: 'doubtful', aliases: ['cigarettes'] },
{ id: 'nft-marketplace', label: 'NFT marketplace', ruling: 'doubtful', patterns: ['\\bnfts?\\b'] },
],
// Bolt on rules that have nothing to do with fiqh
customRules: [{
id: 'internal.sanctions',
evaluate: (subject) => subject.metadata?.sanctioned
? { code: 'sanctions.listed', severity: 'violation', message: 'On the internal sanctions list.' }
: null,
}],
});Purification
calculatePurification({ income: money(1000, 'USD'), nonCompliantShare: 0.023 });
// { amountToPurify: 23.00, retained: 977.00,
// note: 'Give this away without expecting reward. It is not sadaqah and
// should not be claimed as a charitable deduction.' }Rounded up, never down.
Riba and gharar
checkRiba({ latePayment: { charged: true, accruesToLender: true } });
// violation: a late charge kept by the financier is an increase on a debt for time
checkRiba({ fees: [{ name: 'Facility fee', basis: 'time-based' }] });
// violation: a charge for the passage of time is interest whatever it is labelled
checkRiba({ exchange: { give: { asset: 'gold', amount: 10 },
receive: { asset: 'silver', amount: 800 },
settlement: 'deferred' } });
// violation: riba al-nasiah — same ribawi category, so settlement must be spotanalyseContract({
subjectMatter: { description: 'Wheat, 10t, grade A', forwardContractType: 'salam',
existsAtContract: false, ownedBySeller: true,
quantitySpecified: true, qualitySpecified: true, deliverable: true },
price: { specified: true, paidInFullAtContract: true },
delivery: { dateSpecified: true, placeSpecified: true },
});
// ghararLevel: 'none' — a valid salam, even though the goods do not yet existProfit and loss sharing
shareProfitAndLoss({
currency: 'USD',
structure: 'musharakah',
partners: [
{ name: 'Bank', capital: 70_000, profitShare: 0.4 },
{ name: 'Entrepreneur', capital: 30_000, profitShare: 0.6 },
],
result: -10_000, // a loss
});
// Loss: 7,000 / 3,000 — by CAPITAL, not by the 40/60 profit ratio.
// That is not configurable, because a partnership that allocates loss any
// other way has stopped being a partnership.A guaranteedReturn on any partner produces a violation. A mudarib bears no
financial loss unless negligent: true is recorded.
Money
import { money, allocate, split, sum } from 'sharia-compliant-finance';
money(1.005, 'USD').minor; // 101 — the half cent binary floats lose
money(1.234, 'KWD').minor; // 1234 — three decimals, correctly
split(money(100, 'USD'), 3); // 33.34 / 33.33 / 33.33, summing to exactly 100.00Every amount is an integer count of minor units. Currency exponents follow ISO
4217 (JPY 0, KWD/BHD/OMR 3, everything else 2); registerCurrency adds your
own. allocate uses largest-remainder, which is why every schedule in this
package reconciles.
Reading results
Nothing returns a bare boolean. Every check produces findings:
{
code: 'murabaha.sold-before-ownership',
severity: 'violation', // 'info' | 'warning' | 'violation'
category: 'financing',
message: '...',
evidence: { acquiredAt: '2026-04-05', contractDate: '2026-03-31' },
reference: 'AAOIFI SS 8 cl. 3/1/1',
remediation: 'Complete the purchase leg before executing the sale.',
}Verdicts: compliant · requires-review (a warning needs a human) ·
insufficient-data (inputs were missing — not a pass) · non-compliant.
Compose several checks into one report:
generateComplianceReport({
reference: 'ORDER-4471',
screening: { industry: 'electronics retail' },
riba: { kind: 'sale', principal: money(3600, 'AED'), repayment: money(3888, 'AED') },
financing: plan,
});API surface
Financing — createMurabaha createIjara createTawarruq
createDiminishingMusharakah quoteEarlySettlement calculateLatePaymentCharge
rescheduleFinancing outstandingBalance overdueInstallments
effectiveAnnualRate
Zakat — calculateZakat calculateNisab valueMetal
calculateLivestockZakat calculateAgricultureZakat calculateSadaqah
MADHHAB_PROFILES
Screening — screen createScreeningEngine definePolicy
calculatePurification ActivityTaxonomy AAOIFI_POLICY DJIM_POLICY
SP_SHARIAH_POLICY MSCI_ISLAMIC_POLICY FTSE_SHARIAH_POLICY
Contracts — checkRiba analyseContract classifyRibawi RIBAWI_ASSETS
Partnership — shareProfitAndLoss
Money — money fromMinor add subtract multiply divide allocate
split sum format toDecimalString registerCurrency
Reports — generateComplianceReport summariseVerdict
Everything is fully typed. Import types with import type { ... }.
Scope and limits
This is a calculation and screening tool. It does not issue fatwa and it does not replace your Shariah supervisory board.
Where scholars genuinely differ, the library says so rather than picking a
side — that is what doubtful and requires-review are for. Where a figure
depends on data the library cannot see (a metal price, an issuer's zakatable
asset ratio, whether a lessee was negligent), it asks you for it and warns when
you leave it out. Nothing passes by omission.
References cited in findings point to AAOIFI Shari'ah Standards, OIC Fiqh Academy resolutions, and the published index methodologies. They are pointers for your reviewers, not a claim of certification.
Development
npm install
npm run typecheck
npm test # 105 tests
npm run buildLicense
ISC
