# Business rules — buddyfinance (Buddy Finance)

Evidence date: 2026-10-19. Revision: `master` `8d0ae20`.

Rules below are taken from seed data or from code. Each rule is marked **Documented** or **Inferred**. There is no separate business-rules document in the source repository. Nothing in this file is **Documented** from a specification. Seeded lookup rows are labelled **Inferred** as well: they are data the database project inserts, not a signed policy.

An inferred rule is what the code or seed does. It is not a confirmed business decision.

`origin/RenierDEV` is not included, except where a master rule is known to be contradicted by that branch. See the last section.

## Who may participate

| ID | Rule | Source | Label |
|---|---|---|---|
| BR-001 | A marketplace profile is Lend, Borrow, or BuddyFinance | `ProfileType` seed ids 1–3 | Inferred |
| BR-002 | User roles are Super Admin, Admin, and Buddy | `UserRole` seed ids 1–3 | Inferred |
| BR-003 | Registration of a lender or borrower starts as Pending, with FICA Not Compliant | `RegistrationIntegrator.Register` | Inferred |
| BR-004 | The same email cannot be registered twice | `RegistrationIntegrator.Register` | Inferred |
| BR-005 | Registration forces country South Africa and identification type Identity Document | `RegistrationIntegrator.Register` | Inferred |
| BR-006 | Approving a profile sets the profile (and lender or borrower row) to Approved and sets the user active | `ProfilesIntegrator.ApproveProfile` | Inferred |
| BR-007 | A signed-in user who is not Approved is sent to profile details instead of a dashboard | `HomeController.Index` | Inferred |
| BR-008 | Admin and Super Admin land in the Admin area. A Buddy user lands on the account page | `AccountController.ApiLogin`, `HomeController.Index` | Inferred |
| BR-009 | Only a Super Admin can add Super Admin to the role list when creating a user | `AccountsController.PopulateLists` | Inferred |
| BR-010 | The website LoggedIn policy allows any session user whose id is not 0. It does not check role | `AuthorizeRequirementHandler` | Inferred |

## Identity and contact

| ID | Rule | Source | Label |
|---|---|---|---|
| BR-011 | Genders are Male and Female | `Gender` seed | Inferred |
| BR-012 | Identification types are identity document, passport, and driver's licence. The identity label is stored as "Identiy Document" | `IdentificationType` seed | Inferred |
| BR-013 | Address types are Physical Address and Postal Address | `AddressType` seed | Inferred |
| BR-014 | Employment statuses are Employed, Self-employed, and Unemployed | `EmploymentStatus` seed | Inferred |
| BR-015 | Employment types are Temporal, Contract, and Permanent | `EmploymentType` seed | Inferred |
| BR-016 | Source of funds and source of income are each Salary, Wage, and Other | Seed scripts | Inferred |
| BR-017 | Ethnicity values are African, White, Coloured, Indian, and Asian | `Ethnicity` seed | Inferred |
| BR-018 | An OTP SMS says the recipient is about to create a Buddy Finance profile and includes the OTP | `NotificationsIntegrator.SendOtp` | Inferred |
| BR-019 | Document types are Certified SA ID, 3-months bank statement or bank letter, latest payslip, and proof of residence | `DocumentType` seed | Inferred |

## Loan offers

| ID | Rule | Source | Label |
|---|---|---|---|
| BR-020 | A loan offer cannot be less than R500 | `SaveLoanOffer`: amount `<= 499` is rejected | Inferred |
| BR-021 | A loan offer cannot exceed R8000 | `SaveLoanOffer` | Inferred |
| BR-022 | A loan offer cannot be created when the amount is greater than or equal to available funds | `SaveLoanOffer` | Inferred |
| BR-023 | Creating an offer reduces the lender's available funds by the offer amount and writes a debit | `SaveLoanOffer` | Inferred |
| BR-024 | Updating an offer first credits the previous amount back, then debits the new amount | `SaveLoanOffer` | Inferred |
| BR-025 | Interest on an offer cannot exceed 30 percent | `SaveLoanOffer` | Inferred |
| BR-026 | Offer term type is always months | `SaveLoanOffer` assigns `TermTypeEnum.Months` | Inferred |
| BR-027 | R500–R1499 may run for at most 3 months. R1500–R8000 may run for at most 6 months | `LoanTermBracket` seed, checked in `SaveLoanOffer` | Inferred |
| BR-028 | An offer that already has a loan request cannot be edited | `SaveLoanOffer` | Inferred |
| BR-029 | An active offer cannot be updated | `SaveLoanOffer` | Inferred |
| BR-030 | Offer expiry is the save time plus `LoanOfferExpiryDate` months | `SaveLoanOffer` | Inferred. The setting value was not found in committed config |

## Loan requests and loans

| ID | Rule | Source | Label |
|---|---|---|---|
| BR-031 | A loan request of R500 or less is rejected. The check is amount `> 499` | `LoanRequestIntegrator.RequestLoan` | Inferred |
| BR-032 | A loan request requires banking details | `RequestLoan` | Inferred |
| BR-033 | A new request is status New. Linked offers start as Request Sent | `RequestLoan`; `LoanRequestStatus` and `OfferStatus` seeds | Inferred |
| BR-034 | Offer-link statuses are Request Sent, Request Accepted, Offer Accepted, Rejected, and Not Found | `OfferStatus` seed | Inferred |
| BR-035 | Loan request statuses are New, Pre-Approved, Approved, Rejected, and Expired | `LoanRequestStatus` seed | Inferred |
| BR-036 | Accepting one offer on a request creates a loan in Pending, marks that link Offer Accepted, and marks the other links Rejected | `AcceptLoanOfferRequest` | Inferred |
| BR-037 | The offer balance is reduced by the requested amount when the balance covers the request | `AcceptLoanOfferRequest` | Inferred |
| BR-038 | If the remaining offer balance is greater than 0 and less than R500, that remainder is credited back to the lender | `AcceptLoanOfferRequest` | Inferred |
| BR-039 | A separate accept sets the loan Active and records `DateAccepted` | `LoansIntegrator.AcceptLoan` | Inferred |
| BR-040 | Loan statuses are Active, Paid Up, Pending, Deactive, and Declined | `LoanStatus` seed. The fourth label is "Deactive" | Inferred |
| BR-041 | A New loan request is set to Expired when it is at least 30 days old | `BuddyFinanceExpireLoanRequests` | Inferred |
| BR-042 | Repayment statuses are Paid and Unpaid. Schedule statuses are Pending, Incomplete, and Completed | Seed scripts | Inferred |
| BR-043 | First payment date is the last calendar day of the current month when more than 5 days remain, otherwise the last day of the next month | `DateTimeHelpers.GetNextPaymentDate` | Inferred |

## Price and affordability

| ID | Rule | Source | Label |
|---|---|---|---|
| BR-044 | Interest added in the calculator is the sum, over each period, of the declining principal times `(interest / 6 / 100)` | `BaseIntegrator.LoanCulculator` | Inferred |
| BR-045 | Service fee added to the total is R60 times the duration, from the fee row rather than `GlobalSetup.ServiceFee` | `LoanCulculator`; `FeeType` id 3 amount 60. `GlobalSetup.ServiceFee` is seeded as 0 | Inferred |
| BR-046 | Initiation fee is R140 when the amount is at or below R1000. Above that it is `(amount - 1000) * 0.10 + 160` | `FeeType` id 4; `LoanCulculator` | Inferred |
| BR-047 | The instalment is the total divided by the duration | `LoanCulculator` | Inferred |
| BR-048 | Gross income from R500 to R4999 qualifies for R4000. Gross income from R5000 to R8000 qualifies for R8000 | `LoanEarningBracket` seed | Inferred |
| BR-049 | Derived score is bureau credit score times disposable income divided by the calculated repayment, and cannot exceed 950 | `ScoringIntegrator.GetDerivedScore` | Inferred |
| BR-050 | Disposable income is `(gross monthly income - active bureau balances) * (1 - expense factor)`. Seeded expense factor is 0 | `GetDerivedScore`; `GlobalSetup` insert | Inferred |
| BR-051 | Robot bands are Danger Zone 0–150, Fair 151–350, Good 351–600, and Excellent 600–950 | `RobotScore` seed. Score 600 matches both Good and Excellent | Inferred |
| BR-052 | A fresh credit enquiry charges the credit-scoring fee (R15). Viewing an existing score charges R5 | `ScoringIntegrator`; `FeeType` ids 5 and 8 | Inferred |

## Cash

| ID | Rule | Source | Label |
|---|---|---|---|
| BR-053 | A recorded deposit increases available funds by the deposit amount | `SaveFundsToProfile` | Inferred |
| BR-054 | The first deposit debits an application fee of R150 when `IdentificationTypeId` equals 1, and credits that fee to the platform profile | `SaveFundsToProfile`; `FeeType` id 1. Identification type 1 and profile type Lender are both 1 | Inferred. Requires confirmation that identification type was the intended test |
| BR-055 | The minimum withdrawal is R100 | `RequestCashWithdrawal` | Inferred |
| BR-056 | A withdrawal requires banking details | `RequestCashWithdrawal` | Inferred |
| BR-057 | A requested withdrawal above the available balance is reduced to that balance | `ValidateWithdrawalRequest`, `RequestCashWithdrawal` | Inferred |
| BR-058 | Withdrawal fee is R15 when the amount is at or below R700, otherwise 3 percent of the amount | `FeeType` id 2; `ValidateWithdrawalRequest` | Inferred |
| BR-059 | Withdrawal statuses are Approved, Pending, Rejected, and Cancelled | `CashWithdrawalStatus` seed | Inferred |
| BR-060 | Transaction types are Credit and Debit. Transaction statuses are Success and Pending | Seed scripts | Inferred |
| BR-061 | The platform profile id used for fee credits is `Constants.BuddyFinanceProfileId`. The seed inserts that profile as id 1 with profile number `BF000001` | `SaveFundsToProfile`; post-deployment profile insert | Inferred |
| BR-062 | On the billing day of an approved lender, the monthly process debits the service fee from available funds and credits the platform profile | `BuddyFinance.Billing/Program.cs` | Inferred |
| BR-063 | The billing process compares `BillingDate.Day` with today's day. It does not move a day 31 into a shorter month | `BuddyFinance.Billing/Program.cs` | Inferred |
| BR-064 | An approved lender with available funds below zero is emailed an arrears notice | `BuddyFianance.BillingArrears/Program.cs` | Inferred |
| BR-065 | Lender approval email tells the lender to deposit using the profile number as reference, into a Thorough Capital / Buddy Finance Standard Bank account | `ApproveProfile` | Inferred |

## Sessions and access

| ID | Rule | Source | Label |
|---|---|---|---|
| BR-066 | API access tokens last 14 days | `Startup.Auth.cs` | Inferred |
| BR-067 | The OAuth server allows tokens over HTTP | `AllowInsecureHttp = true` | Inferred |
| BR-068 | Website session idle time is 120 minutes | `Startup.ConfigureServices` | Inferred |
| BR-069 | Account and home controllers allow anonymous access. Other website controllers inherit LoggedIn | `AccountController`, `HomeController`, `BaseController` | Inferred |
| BR-070 | Web API controllers inspected do not declare `[Authorize]` | `BuddyFinance.WebApi/Controllers` | Inferred |

## Conflict with the unmerged branch

| ID | What `master` does | What `origin/RenierDEV` commit subjects say | Label |
|---|---|---|---|
| BR-071 | The lender chooses an interest rate up to 30 percent | Interest entry is removed. A later subject says interest comes from the database, and another says the landing page uses 5 percent as a maximum | Requires confirmation. The unmerged subjects disagree with `master` and, read together, are not one rule |
| BR-072 | Documents are stored from base64 without a photo-versus-PDF rule in `Register` | ID must be a picture. Other documents must be PDF | Requires confirmation |
| BR-073 | Accepting an offer creates the loan without a later admin approve step in `AcceptLoanOfferRequest` | A commit subject says admin approves or declines after the borrower requests the offer | Requires confirmation |

## Unknown / requires confirmation

- The number of months in `LoanOfferExpiryDate`.
- Whether every API caller must present a bearer token.
- Whether FICA Fully Compliant is required before a loan.
- Whether the 30 percent cap, the R8000 cap, the R500 floor, and the R60 service fee are still approved.
- Whether score 600 should be Good or Excellent.
- Whether consoles are supposed to run every day.
