# PRD — buddyfinance (Buddy Finance)

Evidence date: 2026-10-19. Source: `git@bitbucket.org:overdrive/buddyfinance.git`, `master` at `8d0ae20` (2022-10-05, "setup").

There is no prior PRD in Silicon Ark and no product specification in the source repository. Requirements below are taken from seeded lookup data, integrators, controllers, and the website. Provenance is marked on each item.

`origin/RenierDEV` (`b144142`) is 14 commits ahead of `master` and is not merged. This PRD does not treat that branch as the product. A short note is in [Unmerged branch](#unmerged-branch).

## Product purpose

Buddy Finance matches people who lend money with people who borrow it, and charges fees for operating the marketplace. Staff approve who may participate, record money in and money out, and control when a loan becomes active.

Provenance: **Inferred** from profile types `Lend`, `Borrow`, and `BuddyFinance` in `Script.PostDeployment1.sql`, and from the loan-offer and loan-request integrators. No document states this purpose in prose.

The public hosts named in configuration are `https://buddyfinance.co.za/qa` (Android WebView) and `https://www.buddyfinance.co.za/service/api/` (`BuddyFinance.Admin/appsettings.json`). Whether those hosts are still live is **Unknown / Requires Confirmation**.

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Lender | Registers a lend profile, is approved, has deposits recorded, creates loan offers, receives withdrawal decisions | Profile type `Lend` (id 1). Website `LenderController` | Implemented |
| Borrower | Registers a borrow profile, is approved, searches offers, requests a loan, has repayments recorded | Profile type `Borrow` (id 2). Website `BorrowerController` | Implemented |
| Buddy (user role) | Signed-in marketplace user. Home sends this role to the account page | `UserRole` id 3, `HomeController.Index` | Implemented |
| Admin | Uses the `Admin` area: profiles, loans, offers, cash, activity | `UserRole` id 2. Login returns the Admin home URL | Implemented |
| Super Admin | Same staff area, and can assign the Super Admin role when creating a user | `UserRole` id 1. `AccountsController.PopulateLists` | Implemented |
| Platform profile | System account `BF000001`, name Buddy Finance, email `admin@buddyfinance.co.za`. Receives fee credits | Post-deployment seed, profile type `BuddyFinance` (id 3) | Implemented |
| Anonymous visitor | Can open the home page, register, and log in | `HomeController` and `AccountController` are `[AllowAnonymous]` | Implemented |

A person is not a separate role from their profile. User roles and profile types are different lists. Lender and borrower are profile types. Buddy, Admin, and Super Admin are user roles.

Seeded personal data on the platform profile (identification number, and the phrase stored as other source of income) is not repeated here.

## Main workflows

### W1 — Register

Trigger: a visitor submits registration. API `POST api/registration/register`. Website validation steps are `ApiRegisterValidate1`, `ApiRegisterValidate2`, and `ApiRegisterValidate3`.

Implemented behaviour (`RegistrationIntegrator.Register`):

- The email must not already be a username. Duplicate email returns "Email address already in use".
- A profile number is generated. Status is Pending. FICA status is Not Compliant.
- Country of birth and address country are set to South Africa. Identification type is set to Identity Document. The request does not choose these.
- Profile type Lender creates a `Lender` row and sets `LendServiceAccount`. Profile type Borrower creates a `Borrower` row and sets `BorrowServiceAccount`.
- Optional banking, next of kin, employment, addresses, and base64 documents are stored. Document bytes are kept in `FileData`.
- `BillingDate` is not set on this path. The assignment is commented out.

OTP: `POST api/registration/sendotp` and `validateotp`. SMS body is "You are about to create a profile on Buddy Finance. Use OTP: {otp}" via SMSPortal.

Verify: `POST api/registration/verify` with token, profile number, and username.

Provenance: **Implemented**.

### W2 — Sign in and staff session

Trigger: the website posts credentials. The API exposes `POST api/account/login` and OAuth `POST /Token`.

Implemented behaviour:

- OAuth access tokens expire after 14 days. `AllowInsecureHttp` is true (`Startup.Auth.cs`).
- The website signs a cookie and stores the API access token on the session user.
- Admin and Super Admin are sent to the Admin area. Other roles get HTTP 200 from `ApiLogin` without that redirect.
- A signed-in user whose profile is not Approved is sent from home to profile details.
- A Buddy user who is Approved is sent to the account page. Any other approved role is sent to the Admin home.
- Session idle timeout is 120 minutes. The LoggedIn policy succeeds when `SessionManager` has a user id other than 0.
- Login writes an activity-log row.

Web API controllers do not carry `[Authorize]`. The pipeline adds `HostAuthenticationFilter` for the OAuth authentication type. Whether every API action checks the bearer token is **Unknown / Requires Confirmation**. Provenance of the routes and the website policy: **Implemented**.

### W3 — Staff approve, reject, or suspend a profile

Trigger: staff call `POST api/profiles/approve` or the Admin profile screens.

Implemented behaviour (`ApproveProfile`):

- Approve sets profile, lender, and borrower status to Approved, writes `ProfileApproval`, sets the linked user `IsActive`, and emails the applicant.
- A lender approval email appends deposit instructions: legal entity Thorough Capital, bank Standard Bank, account name Buddy Finance, branch Greenstone. The account number and branch code are hardcoded in this method and are not repeated here.
- Reject and suspend paths exist on the same integrator (`Suspend`, `ReviewAccount`, `SuspendAccount`) and have matching email templates.

Provenance: **Implemented**.

### W4 — Record a deposit and create a loan offer

Trigger: staff record a deposit (`SaveFundsToProfile`, API `POST api/managecash/deposit`). A lender then saves an offer (`SaveLoanOffer`).

Implemented deposit behaviour:

- Available funds increase by the deposit amount. A cash-deposit row and a credit transaction are stored.
- On the first cash deposit, if `IdentificationTypeId` equals `ProfileTypeEnum.Lender` (both are the number 1), an application fee is debited from the lender and credited to the platform profile. Identification type 1 is "Identiy Document" in the seed, and profile type 1 is Lend. Whether this comparison was meant to test profile type is **Unknown / Requires Confirmation**.

Implemented offer behaviour:

- The offer amount must be greater than or equal to R500 and less than or equal to R8000, and must be less than available funds. Creating an offer debits available funds and writes a debit transaction.
- Interest cannot exceed 30 percent. Term type is forced to months. Term cannot exceed the matching `LoanTermBracket` duration.
- An offer that already has a request cannot be edited. An active offer cannot be updated.
- Expiry is `now` plus `LoanOfferExpiryDate` months from application settings. That key was not found in any committed config file. A missing value would throw when an offer is saved. The configured number of months is **Unknown / Requires Confirmation**.

Seeded term brackets: R500–R1499 for 3 months; R1500–R8000 for 6 months.

Provenance: **Implemented**, except the two confirmation items.

### W5 — Request a loan and accept an offer

Trigger: a borrower calls `RequestLoan` (`api/loans` request routes) after choosing offers. Acceptance is `AcceptLoanOfferRequest`.

Implemented behaviour:

- Amount must be greater than R499. Otherwise the message is "Loan Request less than 500 not allowed."
- Banking details are required. Otherwise `MustUpdateBanking` is set.
- The request is status New. Each selected offer is linked as Request Sent. The borrower and each lender are emailed.
- Accepting one request-offer sets that offer active, reduces the offer balance by the request amount, creates a loan in status Pending, sets the request to Approved, sets the chosen offer link to Offer Accepted, and sets every other offer link on that request to Rejected.
- If the remaining offer balance is greater than 0 and less than R500, that remainder is credited back to the lender with a "Loan Offer Refund" email.
- The lender is emailed with template `AcceptLoanOffer` when the request amount equals the offer amount, otherwise `AcceptLoanOfferB`.
- `AcceptLoan` later sets the loan status to Active and sets `DateAccepted`. The Admin loans screen exposes accept, repayment, payout, statement, and deactivate actions.

`BuddyFinanceExpireLoanRequests` sets a request that is still New to Expired when `(now - RequestedDate)` is at least 30 days. It does not notify anyone. Provenance: **Implemented**.

### W6 — Price a loan

Trigger: `POST api/profiles/getloanscalculations` or acceptance, both calling `BaseIntegrator.LoanCulculator`.

Implemented formula, for duration in the same unit the caller passes (offers force months):

- Monthly interest rate used in the loop is `Interest / 6`.
- Principal reduces by `Amount / Duration` each period.
- Interest for period `i` (starting at 0) is `(Amount - reduction * i) * (Interest / 6 / 100)`.
- Service fee is the seeded service-fee amount times duration.
- Initiation fee is the flat fee when amount is less than or equal to the fee basis; otherwise `(amount - basis) * percentage + amountForPercentUse`.
- Instalment is total divided by duration.
- First payment date is the last day of this month when that day is more than 5 days away, otherwise the last day of next month (`GetNextPaymentDate`).

Seeded fees used here: service fee R60; initiation fee R140, 10 percent, amount-for-percent R160, basis R1000.

An older formula is commented out above this loop: `(amount * (interest / 100)) / 6` times duration. Provenance: **Implemented**. Whether the divisor 6 is a business decision or a fixed six-month assumption is **Unknown / Requires Confirmation**. `origin/RenierDEV` commit subjects say the formula was changed again; that change is not on `master`.

### W7 — Score a borrower

Trigger: `ScoringIntegrator.GetProfileScoring`.

Implemented behaviour:

- The TransUnion MEF integrator sends a consumer enquiry and stores request and response XML on `RequestLog`.
- A new enquiry debits the credit-scoring fee. Viewing an existing score debits `ViewExistingCreditScore`.
- `GetDerivedScore` uses active bureau account balances, gross monthly income, `GlobalSetup.ExpenseFactor`, and `Financial.Pmt` with `interest / GlobalSetup.InterestType`. The result is bureau credit score times disposable income divided by the repayment, capped at 950.
- Seeded `GlobalSetup` has `InterestType` 1 and `ExpenseFactor` 0. Seeded robot bands are Danger Zone 0–150, Fair 151–350, Good 351–600, Excellent 600–950. The Good and Excellent bands both include 600.

Provenance: **Implemented**. TransUnion subscriber codes in the seed are not repeated here.

### W8 — Withdraw cash

Trigger: lender `POST api/managecash/requestwithdrawal`. Staff accept or reject from the Admin cash-withdrawal screens.

Implemented behaviour:

- Banking details are required.
- Minimum request is R100.
- If available funds are lower than the request, the amount is reduced to the balance.
- Withdrawal fee is the flat amount when the amount is less than or equal to the fee basis, otherwise amount times percentage. Seed: R15 flat up to R700, otherwise 3 percent.
- The request is stored and a "Request Cash Withdrawal" email is sent. Staff acceptance and rejection have their own templates.
- Insufficient funds tells the lender to delete inactive loan offers and try again. Creating an offer already reduces available funds, so those two balances are linked.

Provenance: **Implemented**.

### W9 — Monthly service fee and arrears

Trigger: someone runs the console executables. No scheduler is in the repository.

`BuddyFinance.Billing` selects approved lenders. When `BillingDate.Day` equals today's day, it subtracts the service fee from available funds, writes a debit, and credits the platform profile. The platform profile is excluded by profile type. February and short months are mentioned in a comment ("accommodate 28, 29, 30 and 31") but the code compares day numbers only. A lender whose billing day is 31 is not billed in a 30-day month. Provenance: **Implemented**. Whether `BillingDate` is populated for lenders who only use `Register` is **Unknown / Requires Confirmation**, because that assignment is commented out. `UpdateDetails` sets `BillingDate` to now only when it inserts a new profile row.

`BuddyFianance.BillingArrears` emails every approved profile that has a lender row and negative available funds, using template `AccountArrears`. The email subject passed to `SendEmail` is an empty string.

`BuddyFinanceExpireLoanRequests` is described in W5.

### W10 — Statements and reports

Trigger: website statement actions and `GET/POST api/reporting/statement`.

`ReportServiceIntergrator` produces a PDF. SSRS reports `Statement.rdl` and `Transactions.rdl` target a local report server. Debug deploys to folder `BuddyFinance/QA/`. Provenance: **Implemented**. Whether a report server is deployed outside this repository is **Unknown / Requires Confirmation**.

## Functional requirements

### FR-001 — Two marketplace profile types

The system stores a profile as Lend, Borrow, or BuddyFinance.

Evidence: `ProfileType` seed; `ProfileTypeEnum`; `RegistrationIntegrator`.

Implementation: Implemented.

Confidence: High.

### FR-002 — Registration creates a pending profile

A new registration is Pending and FICA Not Compliant until staff approve it. Email must be unique.

Evidence: `RegistrationIntegrator.Register`; `FicaStatus` seed.

Implementation: Implemented.

Confidence: High.

### FR-003 — South Africa and identity document are fixed at registration

Registration sets country of birth, address country, and identification type. It does not take them from the request.

Evidence: `RegistrationIntegrator.Register`.

Implementation: Implemented.

Confidence: High.

The lookup tables still contain passport, driver's licence, and other countries. Whether those values are reachable on another screen is **Unknown / Requires Confirmation**.

### FR-004 — OTP by SMS

The system can send and validate an OTP to a phone number through SMSPortal.

Evidence: `RegistrationIntegrator.SendOtp`, `ValidateOtp`; `NotificationsIntegrator.SendSMS`.

Implementation: Implemented.

Confidence: High.

### FR-005 — Staff approval activates the user

Approval sets Approved, activates the user, and emails the applicant. Lender email includes deposit instructions.

Evidence: `ProfilesIntegrator.ApproveProfile`.

Implementation: Implemented.

Confidence: High.

### FR-006 — Loan offer limits

A lender can create an offer from R500 to R8000, at no more than 30 percent interest, for no longer than the term bracket, only when available funds cover the amount.

Evidence: `LoanOffersIntergrator.SaveLoanOffer`; `LoanTermBracket` seed.

Implementation: Implemented.

Confidence: High.

### FR-007 — Loan request limits

A borrower can request more than R499 only with banking details, against one or more existing offers.

Evidence: `LoanRequestIntegrator.RequestLoan`.

Implementation: Implemented.

Confidence: High.

### FR-008 — Accepting an offer creates a pending loan

Acceptance creates one pending loan, rejects the other offers on that request, and refunds an offer remainder below R500.

Evidence: `LoanOffersIntergrator.AcceptLoanOfferRequest`.

Implementation: Implemented.

Confidence: High.

### FR-009 — A loan becomes active by a separate accept

`AcceptLoan` sets status Active. It does not itself create the repayment schedule. Schedule, repayment, and payout are separate methods.

Evidence: `LoansIntegrator.AcceptLoan`, `Schedule`, `SaveLoanRepayment`, `AllocatePayout`.

Implementation: Implemented.

Confidence: High.

Who is allowed to call accept versus the borrower is **Unknown / Requires Confirmation**. The Web API action `POST api/loans/accept/{loanId}` has no role attribute.

### FR-010 — Loan price

The calculator returns monthly payment, total, initiation fee, interest, service fee, and first payment date using the formula in W6 and the fee rows.

Evidence: `BaseIntegrator.LoanCulculator`; `FeeType` seed.

Implementation: Implemented.

Confidence: High.

### FR-011 — Income brackets

Gross monthly income maps to a qualifying amount: R500–R4999 qualifies for R4000; R5000–R8000 qualifies for R8000.

Evidence: `LoanEarningBracket` seed; `GetProfileEarningBracket`; `GetLoanOffersByAmount`.

Implementation: Implemented.

Confidence: Medium. The offer-search method contains several commented alternatives. The active branch should be re-read before changing qualification behaviour.

### FR-012 — Credit score

A score can be fetched from TransUnion and stored, and a derived score is capped at 950. Robot colour bands are seeded.

Evidence: `ScoringIntegrator`; `Integrator` in the TransUnion project; `RobotScore` seed.

Implementation: Implemented.

Confidence: High.

### FR-013 — Cash withdrawal

A withdrawal requires banking details, at least R100, and enough available funds after the withdrawal fee. Staff can accept, reject, or the lender can cancel.

Evidence: `ManageCashIntergrator`; `CashWithdrawalStatus` seed (Approved, Pending, Rejected, Cancelled).

Implementation: Implemented.

Confidence: High.

### FR-014 — Fees move money to the platform profile

Application fee, service fee, scoring fees, and withdrawal fees are amounts on `FeeType`. Several flows credit profile id of the Buddy Finance profile.

Evidence: `FeeType` seed; `SaveFundsToProfile`; `BuddyFinance.Billing`; `ScoringIntegrator`.

Implementation: Implemented.

Confidence: High.

Seeded amounts: application R150; withdrawal R15 or 3 percent over R700; service R60; initiation as in W6; credit scoring R15; first withdrawal R0; other R0; view existing score R5.

### FR-015 — Documents required by type

Document types are Certified SA ID, 3-months bank statement or bank letter, latest payslip, and proof of residence.

Evidence: `DocumentType` seed; `DocumentTypeEnum`.

Implementation: Implemented as lookup values and upload APIs (`api/files`, profile document routes).

Confidence: Medium. `master` does not show a rule that all four must be present before approval. `origin/RenierDEV` commit subjects add file-type rules that are not on `master`.

### FR-016 — FICA statuses

FICA can be Not Compliant, Partially Compliant, or Fully Compliant. Registration starts at Not Compliant. `UpdateFica` persists a compliance row.

Evidence: `FicaStatus` seed; `RegistrationIntegrator`; `ProfilesIntegrator.UpdateFica`.

Implementation: Implemented.

Confidence: Medium. The rule that maps documents to Partially versus Fully was not found as an explicit check.

### FR-017 — Notifications

Email templates exist for registration, approval, rejection, suspension, password reset, loan request, offer created, offer accepted, offer rejected, offer refund, offer deleted, loan approved, loan declined, loan deactivated, cash withdrawal requested, approved, and rejected, account arrears, and contact us.

Evidence: `BuddyFinance.Integration/Notifications/*.txt`.

Implementation: Implemented. Delivery is SMTP, with a SendGrid method also present.

Confidence: High.

### FR-018 — Staff activity log

Logins and an administration activity API record activity. Admin has an activity-log screen.

Evidence: `AccountController.ApiLogin`; `AdministrationIntegrator`; `ActivityLogController`.

Implementation: Implemented.

Confidence: High.

### FR-019 — Android shell

The Android app shows the website in a WebView at `https://buddyfinance.co.za/qa` when the device is online, and a local error page when it is not.

Evidence: `MainActivity.java`.

Implementation: Implemented.

Confidence: High.

### FR-020 — Loan requests expire

A console sets New requests to Expired after 30 days.

Evidence: `BuddyFinanceExpireLoanRequests/Program.cs`; status Expired is seeded.

Implementation: Implemented. Scheduling is **Unknown / Requires Confirmation**.

Confidence: High for the rule, low for operations.

## Integrations

| System | Purpose | Direction | Trigger | Failure behaviour |
|---|---|---|---|---|
| SQL Server | System of record | Read and write | Every integrator | Exceptions returned as error codes on API results, or printed by consoles |
| TransUnion Consumer SOAP | Credit profile | Outbound | Scoring | Request log stores the exception; the exception is rethrown |
| SMSPortal | OTP and SMS | Outbound | Registration OTP and `SendSMS` | Logged; method returns false |
| SMTP | Email | Outbound | Most workflow emails | Logged; method returns false. Port 587. SSL enablement is commented out |
| SendGrid path | Alternate email | Outbound | `SendEmailSendGridAsync` | Present on `master`. Which callers use it versus SMTP is **Unknown / Requires Confirmation** |
| SSRS | Statements | Outbound from report project | Statement actions | Server URL in the report project is localhost |
| Firebase | Crash and performance on Android | Outbound | Mobile app | `google-services.json` is committed. Keys are not copied here |
| Application Insights | API telemetry | Outbound | Web API | Packages and `ApplicationInsights.config` are present. Whether a key is deployed is **Unknown / Requires Confirmation** |

No card acquirer, debit-order provider, or Stitch client exists on `master`. Stitch appears only on `origin/RenierDEV`.

## Important constraints

- Currency amounts in messages are rand (R). Provenance: **Implemented** in validation messages.
- Loan offer term type is months even if the caller sends another term type. Provenance: **Implemented**.
- OAuth allows insecure HTTP. Provenance: **Implemented**.
- Web API authorization attributes were not found. Provenance: **Implemented** as absence of attributes. Do not treat that as an approved decision to leave the API anonymous.
- Secrets are committed (SQL passwords, and TransUnion codes in seed data). They are intentionally omitted from this PRD.

## Unmerged branch

`origin/RenierDEV` is not the baseline of this PRD. Commit subjects say interest entry was removed, interest defaults to 5 percent and is described as a maximum, interest is loaded from the database, repayment maths changed, document types gained photo-versus-PDF rules, and an admin approve-or-decline step was added to the loan process. The diff adds Stitch token models and a cron-job API. Whether that branch replaced the `master` loan process in production is **Unknown / Requires Confirmation**.

## Unknown / requires confirmation

- Live website, API, and database catalog (`BuddyFinanceDb_Prod` in the website settings versus `BuddyFinanceDB_QA` in the integration config).
- Whether `origin/RenierDEV` is the intended product.
- Months in `LoanOfferExpiryDate`, and where that setting is supplied at runtime.
- Whether the divisor 6 in the interest loop is still the approved price.
- Whether profile approval requires a full document set and a FICA status.
- Whether API routes require a bearer token in a host configuration that is not in git.
- Whether the three consoles are scheduled.
- Whether Thorough Capital and the hardcoded Standard Bank account are still in use.
- Whether the platform profile's seeded available funds of 1000 are still meaningful.
