FLIN product guide
This is the product view of FLIN for the FLIN product team, and for engineers who want to know what the app does before they read the code. It covers what is live in Indonesia, what is built but switched off, where the Philippines stands, which vendor powers each feature, where the copy lives, and what FLIN still owes the build. Terms follow the glossary in CONTEXT.md (Home country, Test user, Internal user, Emailed report, Debt Discipline Program and so on). The binding multi-country rules are in multi-country.md. This guide links to its section numbers rather than repeating them.
Last verified: 2026-10-12 against main @ a270f1d (live infra/EAS/Trigger.dev checks on the same date).
Contents
- What FLIN is
- One app, two countries
- Status board
- Feature inventory
- User journeys
- Notifications
- Analytics and CRM
- Test users and internal users
- Languages and copy
- App Store and Google Play
- Known product-visible gaps
- Pending inputs from FLIN
- Roadmap signals in the repo
1. What FLIN is
FLIN is a credit-wellness app. A user signs up with a WhatsApp-enabled phone number and verifies their identity with an ID-card photo and a liveness selfie. FLIN then shows a credit score and a list of their loans. From there FLIN offers ways out of debt:
- Free credit score: an AFPI (fintech-lender association) report pulled through Finfra once, during onboarding. Indonesia only.
- Paid credit report: the CLIK credit-bureau report, bought per pull. It unlocks every debt category, an Emailed report and a Downloaded report.
- Recommended debt solution: Dana Talangan, a bridging or consolidation loan (wire name
bridgingLoan; the planned Philippine copy calls it "Debt Consolidation"), or Mediasi Utang, debt mediation and negotiation. The user applies in the app and FLIN's team works the application in the CRM. - Debt Discipline Program: a paid repayment-coaching membership. It includes a dashboard, a payment calendar, AI analysis, consultations with a credit expert and a monthly bureau refresh.
Who it is for: consumers with several loans (cash loans, pay later, credit cards). The FAQ describes Dana Talangan as "best suited for salaried employees who have 2 or more active loans, whose total monthly instalments exceed 30–40% of their salary" (en.json, faq → Dana Talangan). The consultation form asks for a service region in Indonesia: Jabodetabek, Bandung, Surabaya, elsewhere in Java, or elsewhere in Indonesia (constants.ts SERVICE_REGIONS).
Names you will see:
- The app's display name is "Flin". The store listing is "FLIN - Credit Wellness App", and the App Store seller is Maybright Asia Pacific pte ltd (verified live 2026-10-12).
- The Terms name PT FLIN Consultancy Indonesia as the contracting entity.
- Bundle id and package:
com.flin.flinapp.
2. One app, two countries
One store binary serves every country. In product terms:
- Home country comes from the device's Region setting: Philippines when the region is PH, Indonesia for anything else. Nobody picks it, and the app never uses location or the SIM card. It is frozen when the user signs in and holds until they sign out. A user who signed in before the PH change stays on Indonesia even on a PH-region phone. See ADR-0009 and multi-country.md §8.3.
- The Home country decides which FLIN server the app talks to. The Indonesia and Philippines servers are separate, each with its own database in its own region. It also decides the dial code (+62 / +63), currency, languages, onboarding steps and products. The dial code and languages come from the app's own country table before sign-in; after that the app follows the server's public
config.getresponse. - A country is offered only when its server URL is baked into the build. The Philippines URL variable
EXPO_PUBLIC_SERVER_URL_PHis not set in any EAS environment, so no shipped binary offers the Philippines. A PH-region phone running the live 1.1.0 build gets Indonesia (verified live 2026-10-12). - Country differences go through data: a country profile, a copy overlay or a vendor adapter. Code never carries a
country === "PH"branch. An ID-only screen is hidden by the feature that owns it, never deleted (multi-country.md §8.4).
3. Status board
Live values were read on 2026-10-12: the public config.get of both servers, the running ECS task definitions (ID flin-prod:282, PH flin-ph-prod:4), and the EAS environment that built the live 1.1.0 binary. Indonesia's store app 1.1.0 has been live since 2026-10-07.
How a feature is switched on. There are four kinds of switch:
- Build flag (
EXPO_PUBLIC_*): frozen into each binary. Changing it needs a new store build. Defined in apps/native/lib/phase1.ts. - Server env: set per deployment. It changes with a redeploy and needs no app release.
- Profile feature fact:
features.*in constants.ts. A fact about the country, changed in code. - Onboarding step:
onboardingStepsin the same file.
Paid features need both the build flag and the server env on.
| Feature | Indonesia (live) | Philippines | Controlled by |
|---|---|---|---|
| Country offered to users | Yes (https://appapi.flin.co.id) |
No. The server is up at https://appapi.flin.ph as APP_ENV=staging, but no binary has its URL |
Build: EXPO_PUBLIC_SERVER_URL / EXPO_PUBLIC_SERVER_URL_PH (EAS) |
| Phone sign-up, WhatsApp OTP | Live (CampaignHQ) | Wired, same adapter; PH account and template not confirmed | Profile adapter (otp) |
| Identity check (ID card + liveness selfie) | Live: KTP read by AWS Bedrock, AsliRI liveness and biometric match (pass ≥ 70) | Mock adapters, always pass. Onfido is pending | Profile adapters (idDocOcr, biometric) |
| Consultation form (financial details) | Live, onboarding step | Not in the flow | Onboarding step financial_details |
| Loan verification screen | Off (retired 2026-09-16, FLIN-208) | Off | Feature fact loanVerification=false |
| Free credit score (AFPI) | Live | Off (no vendor) | Feature fact freeCreditScore (ID true, PH false) |
| Paid credit report (CLIK) | Live at Rp 25.000. Built with EXPO_PUBLIC_PAID_REPORT=1; server PAID_REPORT_ENABLED=1, REPORT_PRICE_IDR=25000 |
Off: PAID_REPORT_ENABLED=0, CIBI adapter not built. Placeholder price ₱199.00 (REPORT_PRICE_MINOR=19900) |
Build flag + server env + price env |
| Emailed report / Downloaded report | Live (follows the paid report) | Off | Follows the paid report |
| CLIK's own score as the headline score | Off (CLIK_BUREAU_SCORE_ENABLED unset, defaults to 0), so FLIN's combined score shows |
n/a | Server env |
| AI loan classification of CLIK contracts | On (CLIK_CLASSIFY_ENABLED defaults to 1) |
n/a | Server env |
| Debt Discipline Program | Server on, store app off. DEBT_DISCIPLINE_ENABLED=1, DDP_REFRESH_ENABLED=1, DDP_DUE_NOTIFS_ENABLED=1, but 1.1.0 was built with EXPO_PUBLIC_DEBT_DISCIPLINE=0, so store users never see it. Internal preview builds (flag 1) see it against production. The due-notification job reported 3 members |
Off (0/0/0). Placeholder prices ₱99.00 / ₱249.00 |
Build flag + server env |
| Consultations (coach calls) | Part of Debt Discipline (same state) | Off | Debt Discipline |
| Dana Talangan application | Live (FLIN One tab, when recommended) | Code shared. The PH recommendation may only offer Debt Consolidation or Debt Discipline; no PH copy yet | Recommendation service |
| Mediasi Utang application | Live | Off (routes return NOT_FOUND) | Feature fact mediation |
| Portfolio Insight (free AI card, Report tab and Discipline dashboard) | Live (Bedrock) | Would likely fail: the Bedrock model agreement for GPT-5.6 Luna is not accepted in the PH account (verified live) | Always on |
| Inbox | Live | Live (no traffic) | — |
| Lifecycle push / WhatsApp nudges | On (NOTIF_TICK_ENABLED=1) |
Off (0), and there is no PH Trigger.dev project |
Server env + Trigger.dev |
| Due-date reminders (inbox) | On (NOTIF_DUE_REMINDERS_ENABLED=1) |
Off (0) |
Server env |
| Application-status updates (from CRM stage) | Live | GHL secrets are probably placeholders | GHL webhook + server |
| CRM sync to GoHighLevel | Live | GHL secrets are probably placeholders (still at version 1) | Server env |
| Support chat | Live (LeadConnector widget) | Same Indonesia widget, which is hardcoded in the app | App code |
| Account deletion | Request through support chat only. No backend | Same | — |
| Languages | Indonesian (default) + English, with a language switcher | English only, no switcher | Profile locales + app country table |
| Adjust in-app funnel events | Live (production environment baked into 1.1.0) | Same SDK | Build: EXPO_PUBLIC_ADJUST_ENV |
| Adjust server-side purchase event | Not sent. ADJUST_S2S_TOKEN is missing from the live task definition |
Placeholder secret | ECS task-def secret |
| Forced or optional update prompt | Not configured (MIN_SUPPORTED_APP_VERSION, LATEST_APP_VERSION, IOS_APP_STORE_ID unset) |
Not configured | Server env |
| Store-review test login | Live in production | Live (its own +63 test list, as set in Terraform) | Server env TEST_PHONE_NUMBERS, TEST_OTP_CODE |
| Internal-user free purchases | Verified @flin.co.id email |
Nobody (no staff domain yet) | Profile internalEmailDomain |
⚠️ Unverified: whether the PH vendor secrets (Xendit, GHL, Adjust, Sentry) are still placeholders. They are at SSM version 1, written in the same second as the Terraform apply that seeds
https://replace.meplaceholders. Nobody read the values (live metadata 2026-10-12).
4. Feature inventory
Screens are files under apps/native/app/. Prices are the live values from §3.
4.1 Sign-up, onboarding and identity
- What the user sees:
- Launch screen index.tsx: phone entry with the country dial code, a signup carousel with country-specific art, and the language selector.
- verify.tsx: 6-digit WhatsApp OTP.
- consent.tsx: app consent.
- capture-ktp.tsx: ID-card photo with a guide frame.
- verify-identity.tsx: liveness selfie and biometric match.
- identity-verified.tsx.
- credit-report.tsx: a "create credit profile" intro.
- financial-details.tsx: the consultation form (income, employment, service region).
- notifications.tsx: push permission. Finishing it completes onboarding.
- Rules:
- The steps come from the server (
config.get.onboardingSteps; see §5.1). - Identity has two records: the Scanned KTP (immutable evidence) and the Confirmed personal details (user-correctable). The NIK always comes from the scan.
- A failed biometric match lets the user retake the photo (
kyc.resetKtp).
- The steps come from the server (
- Vendors: CampaignHQ (WhatsApp OTP); AWS Bedrock (KTP reading, runbook); AsliRI (liveness SDK + verify); GoHighLevel (every step syncs to the CRM, §7.2).
4.2 Home, loans and the free credit score (Indonesia)
- What the user sees:
- Home (home.tsx): a score hero, status counts and total outstanding, the loan list (FLIN-211), the recommendation card, an add-loan entry, promo cards and an eye toggle that hides figures.
- Report tab (report.tsx): the full report. It is locked until the paid report is bought.
- loan-detail.tsx: Overview / Amount & Balance / Terms / Repay history tabs. AFPI and manual loans show Overview and Balance only.
- paylater-lender.tsx: a Lender card for closed pay-later draws.
- add-loan.tsx and edit-debt.tsx: manual loans. Only manual loans can be deleted.
- Rules:
- The free AFPI report is pulled once per user, when the consultation form is submitted (a cost guard). Only Debt Discipline members get a monthly AFPI refresh.
- FLIN's own score runs 300–900, in bands excellent / good / fair / subprime / high risk.
- Free tier categories are cash loan and pay later. Every other Debt category is a Locked category until the paid report is bought.
- Cash loan merged KTA and Pinjol on 2026-09-21 (ADR-0010).
- Each loan has a Loan status (active / overdue / closed) and a Status line ("Due on / Overdue since / Paid on / Written off on").
- Loans are listed by Loan amount. A credit card is listed by its outstanding instead.
- The headline score is CLIK's own bureau score (only if that flag is on), then FLIN's combined score, then the CLIK-derived score, then the AFPI-derived score (headline-score.ts).
- Vendor: Finfra (AFPI). The free score is a country fact, server-enforced by
requireFeature("freeCreditScore").
4.3 Paid credit report (CLIK), Emailed report, Downloaded report
- What the user sees:
- A purchase card inside the unpaid loan list and locked category rows (FLIN-241), plus "Pay Once for {price}" buttons (FLIN-243/244).
- confirm-details-paid.tsx: confirm name and address (the NIK is not editable) and verify an email by code.
- activate-program.tsx: CLIK consent, then Xendit hosted checkout in the browser, then a success screen.
- Home's "Refresh Data" goes back through the same checkout.
- Rules:
- Price: Rp 25.000 in Indonesia (
REPORT_PRICE_IDR). The report is bought per pull: "Refresh" is a new purchase, and the server has no "already paid" guard on purpose. - A Verified email is required before buying. The code lasts 10 minutes, with a 30-second resend cooldown and 5 attempts.
- The consent text the user accepted must hash-match a published wording (consent-registry.ts).
- A Xendit session lasts 60 minutes, and a pending one is reused for up to 50 minutes.
- After paying, the app polls for about 45 seconds, then waits up to 60 seconds for the CLIK loans before opening Home.
- The Emailed report is a CLIK-only PDF, encrypted with the date of birth (DDMMYYYY) as password. It is sent after every bureau pull with up to 6 attempts (swept every 15 min) and kept for 12 months.
- The Downloaded report covers CLIK + AFPI, is unencrypted, is rendered live on each tap and opens through a 5-minute link. See ADR-0006.
- CLIK contracts are classified once by an AI model (ADR-0007). A Duplicate loan reported by both bureaus keeps CLIK's record (ADR-0008).
- Price: Rp 25.000 in Indonesia (
- Vendors: CLIK (SOAP enquiry, plus a nightly consent-evidence upload over FTPS, a regulatory obligation); Xendit; AWS Bedrock (classification); SMTP email (Amazon SES; for Indonesia this is a strong inference from live AWS metadata, see operations.md); Adjust (server-side purchase event, currently not sent: §7.1).
- Switches: both halves must be on. Build
EXPO_PUBLIC_PAID_REPORThides the UI. ServerPAID_REPORT_ENABLEDstrips CLIK rows from every response when off.
4.4 Recommendation, Dana Talangan and Mediasi Utang (FLIN One tab)
- What the user sees:
- The FLIN One tab shows only when there is a recommendation or a submitted application.
- Dana Talangan: dana-talangan.tsx (pitch with monthly repayment), then a 3-step application (personal → employment and finance → documents and bank), then review and submit, then a status timeline backed by the CRM.
- Mediation: mediation.tsx (pitch), then financial data with secured and unsecured lists, manual loans and uploads (bank statement, salary slip), then review and submit.
- Rules:
- The recommendation chooses Dana Talangan or Mediasi by FOIR with a 0.65 threshold, using the FLIN instalment: PMT at 4%/month over 36 months on the Effective outstanding × 1.05.
- A submitted application gets a number
FL-YYMM-NNNNN, an inbox entry and a CRM update. - The status screen shows the CRM pipeline stage (§7.2).
- Vendor: GoHighLevel (pipelines, stage webhook).
4.5 Debt Discipline Program (built; hidden in the store app)
- What the user sees:
- The Discipline tab: an intro for non-members, the dashboard for members.
- Promo cards on Home and Report.
- discipline-pricing.tsx → discipline-consent.tsx (program consent + Xendit checkout).
- discipline-calendar.tsx: Due instances and Payment marks.
- The AI analysis and score projection cards, a renewal banner, book-coach.tsx and past-calls.tsx.
- Rules:
- Discipline plans: monthly, 1 month, Rp 35.000; quarterly, 3 months, Rp 99.000. These are the schema defaults; the live
config.getmatches. - A First-time buyer of the quarterly plan gets a Bonus month (pay 3, covered 4). Nothing auto-renews.
- The Renewal window opens 7 days before the end of a monthly plan and 14 days before the end of a quarterly one. After coverage ends, a "plan ended" prompt shows for 30 days. An early renewal appends a new period starting at the old end.
- Discipline membership is derived from purchases and the clock; it is never stored.
- A member gets one CLIK + AFPI refresh per calendar month (daily job at 09:00 WIB) and a monthly AI analysis (Bedrock, cached for the month). The score projection is display-only.
- Rules live in debt-discipline-helpers.ts.
- Discipline plans: monthly, 1 month, Rp 35.000; quarterly, 3 months, Rp 99.000. These are the schema defaults; the live
- Vendors: Xendit, CLIK, Finfra, AWS Bedrock, GoHighLevel (coach calendar).
- Note: the server gates only the entry points (
config.get.features.debtDiscipline). Its purchase procedures carry no server-side flag check. That is deliberate: internal builds buy against the same production server. Whether to add a server gate is an open question (handover.md).
4.6 Consultations (coach calls)
- Rules:
- A Consultation is a video call with a credit expert, booked on the GHL coach calendar through the in-app widget. Its URL is server-held in
DDP_COACH_BOOKING_URLand prefilled with the member's name and phone. - A Consultation credit is granted per paid month (the bonus month is excluded), all upfront.
- A member holds at most one Upcoming or In Progress consultation at a time.
- A consultation allows up to 2 Reschedules (3 slots in all). Cancelling or missing a call does not refund the credit.
- Reminders go out 72 h, 24 h and 1 h before the slot (inbox + push). They are checked every 15 minutes while
DDP_DUE_NOTIFS_ENABLED=1. - Status (Upcoming / In Progress / Completed / Cancelled) follows the clock and the calendar (routers/consultations.ts).
- A Consultation is a video call with a credit expert, booked on the GHL coach calendar through the in-app widget. Its URL is server-held in
4.7 Inbox, profile, support, legal
- Inbox: notification-center.tsx, with All / Payments / Score tabs, mark read and per-type deep links (§6.2).
- Profile: profile.tsx.
- Rows: FAQ, Chat, Terms, Privacy, Language (hidden when the country has one locale), a theme toggle, logout and Delete account. The app version label shows under Terms.
- edit-profile.tsx edits email, salary and avatar. Changing the email resets its verification.
- Hidden "future-phase" sections: Security & Access (change PIN), Payments (method, history), Subscription (status, renewal) and an "Unlock Debt Discipline" upsell card. They are commented out in
profile.tsx.
- Support chat: support-chat.tsx embeds the LeadConnector (GoHighLevel) chat widget in a WebView with base URL
https://flin.co.id. That domain must stay on the widget's allowed-domain list. There is no server code for it. - Delete account: delete-account.tsx explains a hand-off to support chat. Nothing is deleted in-app and there is no deletion backend. The Privacy Policy promises processing within 7 business days.
- Legal: terms.tsx, privacy.tsx and faq.tsx render structured locale blocks. The FAQ has 13 categories and 145 items. See §9.
5. User journeys
5.1 Sign-up and onboarding (Indonesia vs Philippines)
Step lists come from the live config.get (verified 2026-10-12):
- ID: consent, verify_id, verify_identity, credit_report, financial_details, confirm_details, notifications, completed.
- PH: the same, without
financial_details.
verify_id and confirm_details are alias steps with no screen of their own. They exist so that older builds and mid-flow users keep working (onboarding-steps.ts).
Country differences in this journey:
| Step | Indonesia | Philippines (not offered yet) |
|---|---|---|
| ID document | KTP, read by AWS Bedrock | Mock reader returning synthetic KTP fields. Onfido is planned |
| Liveness / match | AsliRI SDK + verify, pass ≥ 70 | AsliRI mock: always passes |
| Consultation form | Yes; it triggers the free AFPI pull | Step absent. But the app's credit-report screen still opens /financial-details directly (gap, §11) |
| Copy | en.json / id.json |
en.json (Indonesia's English) until the en-PH overlay merges |
5.2 Buying the paid report (Indonesia)
5.3 Debt Discipline purchase and renewal
5.4 Consultation booking
6. Notifications
The word covers two separate systems (ADR-0004):
- Outbound notifications are lifecycle nudges to the user, organised as Series, Enrollments and the Send-gate.
- The Inbox is the in-app list.
6.1 Outbound lifecycle series
Defined in packages/notifications/src/series.ts. One arbiter tick runs every 30 minutes (ADR-0002).
| Series | Channel | Who (Persona) | Anchor event | Steps (offset from anchor) |
|---|---|---|---|---|
ktp |
P1b: phone verified, no KTP | phone verified | +2 h, +1 d at 10:00, +3 d at 11:00 | |
photo |
P1b: KTP uploaded, no selfie | KTP uploaded | +2 h, +1 d, +3 d | |
detail |
P1c: identity done, form not done | biometric passed | +2 h, +1 d at 10:00, +3 d at 11:00 | |
upgrade |
Push | P2a: onboarded, not paid. Recurring until paid | reached Home | day 0, +3 d, +7 d, +14 d |
prod |
Push | Onboarded and eligible for a recommendation. Branches on application state | reached Home (not started) / application start (started, incomplete) | not started: +2 d, +6 d. Started: +2 d, +4 d |
ddp |
Push | Debt Discipline member (replaces upgrade and prod while covered) |
plan activated | 0 h, +1 d at 09:00, +3 d at 10:00 |
- Channel split (ADR-0001): early onboarding users have no push token yet (it is captured near the end of onboarding), so they get WhatsApp through CampaignHQ templates. After onboarding they get Expo push.
- Send-gate defaults:
- quiet hours 22:00–07:00
- daily cap 1 for P1 and 2 for P2
- cooling period 5 days
- minimum 2 days between steps in recurring and
prodseries - (
NOTIF_*env, packages/env/src/server.ts)
- Time zone: hardcoded to WIB (Asia/Jakarta) in the notification engine (config.ts), in the jobs' "today" calculation, and in every Trigger.dev cron. A Philippine stack would send at Jakarta times (one hour behind Manila) until this is read from the country profile (multi-country.md §7).
- Live state (2026-10-12):
- ID has
NOTIF_TICK_ENABLED=1; production tick runs report about 86 active enrollments and 0–1 sends per run. - PH has
0, and no PH Trigger.dev project exists.
- ID has
⚠️ Unverified: whether Meta has approved the WhatsApp templates in every language. The template ids are registered in templates.ts, and the code treats a template as "inert until approved". Earlier local notes said the handoff was blocked on Meta approval.
6.2 Event-driven notifications and the Inbox
The inbox stores a type plus params, and the app renders the text in the user's language (ADR-0003). There are 13 types:
| Type | Written when | Also pushed? | Inbox deep link |
|---|---|---|---|
payment_success |
Paid report or Debt Discipline plan settled | Yes | Report |
report_ready, report_refreshed, score_changed, recommendation_available |
After a CLIK pull | No (inbox only) | Report / FLIN One |
application_submitted |
Dana Talangan or mediation submitted | No | FLIN One |
application_status_changed |
CRM stage changes (§7.2) | Push and WhatsApp on milestones, if the user kept "updates" on | FLIN One |
due_date_reminder |
Daily 08:00 WIB, open loans due today (NOTIF_DUE_REMINDERS_ENABLED) |
No (inbox only) | — |
welcome |
Onboarding completed | No | Home |
ddp_due_stage |
Member loan due at T-7/5/3/1, due, D+1/3/7 (daily 08:00 WIB) | Yes | Discipline calendar |
ddp_streak, ddp_milestone |
Member streaks; debt reduced 10/25/50/75/100% | Yes | Discipline |
ddp_consultation_reminder |
72 h / 24 h / 1 h before a call | Yes | Discipline |
- Push taps do not route. The app registers no notification-tap handler, so tapping a push just opens the app (§11). Deep links work only from inbox rows.
- An app build that does not know a newer type still shows it, using the server's fallback text.
Where notification copy lives is listed in §9.2.
7. Analytics and CRM
7.1 Adjust (attribution and funnel)
- The Adjust SDK runs in the app (lib/adjust.ts). The store build uses the production Adjust environment.
- ATT is disabled and IDFA is never used. The app syncs Adjust's install id (adid) to the server and passes the push token to Adjust for uninstall tracking.
- Funnel events (keys are the exact Adjust dashboard names):
- Onboarding:
sign_up_number_verified,app_consent_completed,kyc_completed,free_credit_score_viewed,onboarding_consultation_form_completed,onboarding_loan_data_confirmation_completed(screen retired),registration_complete. - Dana Talangan:
dana_talangan_app_from_started(sic),dana_talangan_app_form_documents_uploaded,dana_talangan_app_form_filled. - Mediation:
m&n_app_form_started,m&n_app_form_documents_uploaded,m&n_app_form_filled. - Paid report:
paid_report_payment_initiated,paid_credit_report_checked,credit_report_download,refresh_credit_report. - Engagement:
loan_details_viewed,my_profile_viewed,customer_support_reached_out,account_deletion_request_sent.
- Onboarding:
- Milestones are Unique events (once per device). Repeatable actions count every time.
paid_report_purchasedis server-to-server only, and it is not reaching Adjust from Indonesia production.- The SSM parameter
/flin/prod/ADJUST_S2S_TOKENwas filled on 2026-08-22, but no live task-definition revision ever injects it, so the server skips the event silently (verified live 2026-10-12, task defflin-prod:282). - Revenue attribution in Adjust is missing for every real purchase until the secret is added to the running task definition. See deployment.md for the clone-the-service-revision procedure.
- The SSM parameter
⚠️ Unverified: Adjust iOS SKAN app verification. KumoHQ's local setup checklist still had it unchecked; see "Deferred / open" in specs/adjust-integration.md.
7.2 GoHighLevel CRM
-
What is synced (crm-sync.ts, field map field-map.ts):
- The app pushes the whole current picture of a contact at every step: OTP, KTP, selfie, biometric, consultation form, onboarding completion, application drafts and submits.
- That picture covers identity and address from the KTP, employment, income and debt totals, Dana Talangan bank details, mediation data and timestamps.
- It includes a revocable 90-day link to the KTP image for CRM reps. Opening it gives a 5-minute download (ADR-0005).
- Test users are never synced.
-
"FLIN App Status" field (numeric): 1 number submitted, 2 OTP verified, 3 KTP, 4 selfie, 5 eKYC done, 6 consultation form, 7/8 Dana Talangan draft/submitted, 9/10 mediation draft/submitted. The app never lowers it. Values 11 (not eligible) and 12 (not responding) are set only by the CRM team (app-status.ts).
-
Pipelines → what the user sees (pipeline-map.ts, keyed by stage id):
-
"FLIN App" (Dana Talangan):
Stage User sees DT / M&N App Form Filled Submitted Credit Interview Done, Credit Process Credit assessment IPA Granted Approved (milestone) Dana Talangan Disbursed Disbursed (milestone) Rejected by FLIN Rejected (milestone) Rejected by Customer Closed (no push) Not Eligible Not eligible (milestone) Not Responding On hold (milestone) -
"M&N Program" (mediation):
Stage User sees M&N App Form Filled Submitted M&N (Credit Funnel) Recommended for mediation (milestone) Leads Contacted Under review Meeting Completed Consultation completed Quotation Sent Quotation sent (milestone) Mediation, Negotiation In progress Rejected / Not Eligible / Customer Not Responding As above -
A Dana Talangan applicant moved to the mediation pipeline is shown the mediation journey.
-
Renaming or reordering stages in GHL breaks the mapping, and a pinned test fails. Tell engineering before changing the pipeline.
-
-
Inbound: a stage-changed webhook and a coach-appointment webhook.
7.3 Metabase
- Metabase (
https://metabase.flin.co.id) reads the Indonesia production database through a read-only role that can see every column, PII included. The Philippines has no Metabase. - Analyst gotchas are in data-model.md:
- Exclude synthetic
report_paymentrows from revenue:ddp-refresh-…(amount 0),internal-…andtest-…. - Exclude Test users.
- Read the latest CLIK report only when the pull completed.
- Exclude synthetic
- Access is currently not restricted. The WAF allowlist that should gate Metabase admits every IPv4 address (verified live 2026-10-12). Restoring the allowlist is an operations fix (operations.md).
- A manual database snapshot named
flin-prod-pre-user-purge-20261006exists (verified live).
⚠️ Unverified (earlier maintainer notes): production was purged to the test users on 2026-10-06, so Metabase history before that date is not representative.
8. Test users and internal users
| Test user | Internal user | |
|---|---|---|
| Who | A phone number on the deployment's TEST_PHONE_NUMBERS list (9 numbers in ID production). Used by app-store reviewers and QA against production |
A member of staff whose verified email is exactly on the company domain: flin.co.id in Indonesia. The Philippines has none yet (pending FLIN-174) |
| Sign-in | Fixed code (TEST_OTP_CODE) instead of a WhatsApp OTP |
Normal OTP |
| Vendors | Identity, AFPI and CLIK are simulated (a synthetic "TEST USER" KTP, mock liveness, a dummy CLIK report even in production). One designated number in code exercises CLIK's published UAT subject for a real bureau answer | Everything real: bureau pulls, identity checks, CRM |
| Money | Purchases recorded as paid, no charge (test- prefix) |
Purchases recorded as paid, no charge (internal- prefix) |
| CRM | Never synced | Synced |
- The store-review login is live in Indonesia production (verified live 2026-10-12): the test-number list and fixed code are on the running task definition. The PH stack carries its own list of Philippine test numbers, matching Terraform.
- A synthetic sign-up address (
…@temp.flin.co.id) never makes anyone internal. - Internal preview builds talk to the production API (verified live, EAS
previewenvironment). Anything a tester does there that is not on a test number creates real production data, CRM contacts and charges.
TODO(handover): Which test numbers and which login instructions were given to Apple and Google for review, and who keeps them current?
9. Languages and copy
9.1 Languages
- On
main:- en.json (Indonesia's English, and the complete fallback), id.json and fil.json.
- The app registers
en,id,filand theen-PHoverlay (lib/i18n.ts, lib/locale.ts); which a user can pick is the country's set. Filipino was withdrawn from Indonesia's set before the first release (DEV-44 / QA-67). - Indonesia opens in Indonesian whatever the phone's language (FLIN-4), and offers English through the language switcher.
- The user's choice is sent to the server, which uses it for push, WhatsApp and report-email language.
- Philippines: English (
en-PH) and Filipino, with the same switcher. Theen-PHoverlay holds only the keys PH words differently (onmainsince PR #127, 2026-10-13); its consent is a placeholder until FLIN sends the Philippine text (FLIN-154). The remaining planned replacements (₱, Debt Consolidation, CIBI, DPD, PHT…) are specified in multi-country.md §8.5. - Legal screen language status:
- The
terms,privacyandfaqblocks exist in all three files with the same structure (13 FAQ categories, 145 items each). - The Indonesian and Filipino blocks were added on 2026-07-08 (commit
9c7af55). - A code comment says the server's Filipino push copy is "an AI translation pending review, matching how the legal screens were handled" (application-status-copy.ts).
- The
⚠️ Unverified: that the Indonesian legal text has had no human legal review. Earlier maintainer notes say the id/fil Terms, Privacy and FAQ are AI translations that still need Indonesian and Filipino legal review before they are treated as binding.
TODO(handover): Has FLIN's legal team reviewed the Indonesian Terms, Privacy Policy, FAQ and consent text shown in the live app?
9.2 Where to change copy
| Copy | Where | Ships how |
|---|---|---|
| App UI text | apps/native/locales/{en,id,fil}.json (every string goes through i18next) |
New store build (there are no over-the-air updates) |
| Terms / Privacy / FAQ | Structured blocks terms, privacy, faq in the same files, rendered by legal-screen.tsx and faq.tsx. Keep the structure identical across the three files; earlier edits were scripted for that reason |
New store build |
| CLIK consent (paid report) | activateProgram.consentBody. Also bump CLIK_CONSENT_VERSION in activate-program.tsx and add the new hashes to consent-registry.ts, keeping the old ones. A test fails if the copy and the hashes drift |
Server + store build |
| Debt Discipline consent | Locale keys used by discipline-consent.tsx. Bump DISCIPLINE_CONSENT_VERSION |
Store build |
| Inbox text | App: notificationCenter.types.<type> in the locale files. Fallback for old builds: inbox-copy.ts |
Store build / server |
Lifecycle push (upgrade, prod, ddp) |
English in series.ts; id/fil in copy.ts (AI translations pending native review) | Server deploy |
| Debt Discipline event pushes | Server fallback text in inbox-copy.ts | Server deploy |
| Application-status pushes | application-status-copy.ts | Server deploy |
| WhatsApp messages | Templates approved by Meta in CampaignHQ; ids per language in templates.ts | CampaignHQ + server |
| Emailed report email and PDF text | report/copy.ts | Server deploy |
| Email verification code email | email-otp.ts (English only) | Server deploy |
| PH wording | apps/native/locales/en-PH.json overlay (not on main yet). Never edit PH wording into en.json / id.json |
Store build |
Prices are not hardcoded in the app UI; it formats config.get prices. Two exceptions:
- The legal and FAQ copy states "Rp25,000" in all three locale files. A price change needs a copy change and a store build. The live price matches it. A comment in
infra/terraform/envs/prod/terraform.tfvarssaying the FAQ reads Rp20.000 is stale. - The
upgradepush series says "Rp29,000" / "Rp29.000" and "Today only", but the price is Rp 25.000 and no discount exists (§11).
10. App Store and Google Play
| Fact | Value (verified live 2026-10-12) |
|---|---|
| Live version | 1.1.0 on both stores. iOS released 2026-10-07 (first iOS release 2026-09-03). Play updated 2026-10-06 (first released 2026-08-22, "10+" downloads) |
| iOS App Store id | 6781172200 |
| App Store storefronts | Indonesia only. Lookups in the PH and US storefronts return nothing |
| Google Play | The listing page loads for ID and PH. Per-country availability was not verified |
| Builds behind 1.1.0 | Commit 2ac4279 (merge of PR #135): iOS build 31, Android versionCode 4 |
| Next version | 1.2.0 is set in app.json. Only two internal Android preview builds exist; there is no store build |
| Over-the-air updates | None. Every app change needs a store build and review |
| Forced or optional update prompt | Built but not used. The server sends no minimum or latest version, so nobody is prompted. The iOS "update" button would open the generic App Store because IOS_APP_STORE_ID is unset; it should be 6781172200. Raising the minimum version is an ops decision; never bump it as a side effect |
- Privacy labels:
- The paid report is the app's only purchase, and it is on in 1.1.0, so "Purchases → Purchase History" should be declared.
- The earlier rule was to drop Purchases only while the paid-report build flag is off.
- Adjust adds identifiers (IDFV) and usage data (analytics / third-party advertising) to the Apple and Play declarations (gitignored spec notes).
- No store metadata or privacy declarations are kept in the repo.
TODO(handover): What do the current App Store privacy label and Play Data Safety form declare, and does it include Purchases now that the paid report is live?
The release process is in mobile-release.md.
11. Known product-visible gaps
Engineering detail and fixes are consolidated in handover.md → Known issues.
| Gap | Impact | Fix direction |
|---|---|---|
| Tapping a push notification opens the app without going anywhere (no tap handler; pushes carry only an id) | Users must find the item themselves; push engagement is lower | Add a tap handler that routes like the inbox (store build) |
/r/verify-id server link redirects to flin://verify-id, which has had no screen since 2026-08-13 |
Anyone following it sees an English "Page Not Found" | Point the server redirect at flin://verify-identity (server-only fix). Check whether any CRM or WhatsApp message uses the link |
upgrade push copy advertises "Rp29,000" and a "Today only" offer |
Live ID users see the wrong price and a discount that does not exist | Correct the copy in series.ts / copy.ts (server deploy) |
| FAQ promises a monthly report refresh for paid-report buyers and a "Profile → Settings → Notifications" screen | Neither exists: the monthly refresh is Debt Discipline-only, and there is no notification-settings screen | Correct the FAQ (store build) |
| No in-app account deletion and no deletion backend; the Privacy Policy promises processing within 7 business days | Requests arrive through support chat with no defined back-office process | Define the ops process (database, files, CRM, Xendit, Adjust); build deletion later |
| Adjust server-side purchase event not sent from ID production | No purchase revenue in Adjust | Inject ADJUST_S2S_TOKEN into the running task definition |
| The old Trigger.dev project (KumoHQ org) still runs notification crons against another database; it sent 1 notification on 2026-10-11. The Trigger.dev staging environment sends real Debt Discipline notifications to 2 members | Stray or duplicate notifications to real people | Disable the old project's schedules (or delete it); check staging's target |
| Internal preview builds use the production API | Test activity pollutes production data and CRM | Use test numbers, or point previews at UAT |
Developer screens asliri-test and modal are reachable by deep link in store builds |
Odd screens if someone guesses the link | Remove or gate them |
| Unknown links land on a hardcoded-English "Page Not Found" | Untranslated screen | Localise it |
| Version gating unset | No way to push users off old builds until configured | Set IOS_APP_STORE_ID; decide minimum and latest versions |
| Email verification code email is English only | Indonesian users get an English email | Localise email-otp.ts |
PH only: money renders only as rupiah (lib/money.ts has no PHP; the price hook hides non-IDR prices instead) |
A PH launch would show amounts wrongly or blank | Phase 1b money hydration (multi-country.md §5) |
PH only: the app does not read features.freeCreditScore or features.mediation (the server enforces them), and the PH "locked Home" (FLIN-171) is not built |
PH users would see ID-shaped Home surfaces whose calls fail | PH screen tickets |
PH only: the credit-report screen opens the consultation form directly, although PH has no such step |
A PH user would hit a step outside their flow | Route from the country's step list |
| PH only: identity verification is mocked (always passes) and the bureau adapter throws | PH must not take real users until Onfido and CIBI land | Adapters; the PH server refuses APP_ENV=prod until then |
| PH only: notification and job times use WIB; there is no PH Trigger.dev project | No PH reminders, retries or nudges; times off by an hour | Country time zone + PH Trigger.dev project |
PH only: support chat uses Indonesia's widget (https://flin.co.id) |
PH chats land in the ID inbox | Widget per country (needs a mechanism, §8.2) |
| PH only: AI features need the Bedrock GPT-5.6 Luna agreement accepted in the PH account | Portfolio Insight and Debt Discipline AI would fail in PH | Accept the offer in the console, or use an already-accepted model |
12. Pending inputs from FLIN
These block work. The rule is: build on mock adapters and do not guess (multi-country.md §8.6, CLAUDE.md). Code markers: TODO(FLIN-174) in profiles/ph.ts, constants.ts and infra/terraform/envs/ph-prod/main.tf.
| Input | Ticket | What it blocks |
|---|---|---|
| CIBI sample response + UAT credentials | FLIN-174 | The PH credit-bureau adapter, the PH paid report, and the KOL→DPD loan-status shape and Pinjol/KTA → "Personal Loan" mapping (profiles/ph.ts). ⚠️ Unverified: earlier maintainer notes record CIBI UAT credentials received on 2026-09-25 and the UAT API answering; no adapter exists on main |
| Onfido contract (classic API access, PhilID/UMID coverage, pricing) | FLIN-174 | PH identity verification (both ports), and flipping the PH server to APP_ENV=prod |
| PH prices: paid report, Debt Discipline monthly and quarterly | FLIN-174 | Live placeholders ₱199.00 / ₱99.00 / ₱249.00 (envs/ph-prod/main.tf) |
| PH staff email domain | FLIN-174 | Internal users in PH (constants.ts) |
| PH app consent text (English and Filipino) | FLIN-154 | The consent screen (placeholder on branch ph-languages) |
| PH credit-report consent text (names PT CRIF/CLIK today) | FLIN-207 | The PH paid-report consent |
| PH Terms, Privacy, FAQ | §8.6 | PH legal screens |
Portfolio Insight prompt (PH_Portfolio_Summary_AI_Prompt_and_Inputs), Debt Discipline AI prompt changes |
§8.6 | PH AI features |
Score projection logic, estimated score range (FLIN APP_Estimated Credit Score) |
§8.6 | PH score surfaces |
Debt Consolidation eligibility (PH Criteria for Product Recommendation) |
§8.6 | The PH recommendation |
| KOL/DPD explainer content | FLIN-174 | "What is DPD?" copy |
| Replacements for OJK / KEMENKEU / Komdigi references (pending designer) | FLIN-174 | PH copy |
| Nav bar: 4 or 5 tabs (pending Isha) | FLIN-174 | PH tab set |
| Xendit PH account confirmation; CampaignHQ PH account and templates; whether GHL PH reuses ID's field and pipeline ids | §8.6 | PH payments, OTP / WhatsApp, CRM |
| Missing PH icons and illustrations (tag Isha + Akshay) | §8.5 | PH screens |
Open Indonesia decisions were raised on the Kumo tickets.
⚠️ Unverified (earlier maintainer notes, not in the repo):
- FLIN-211 loan-list label sign-offs: "Loan Amount" labels, "Written off/Macet" on KOL 5, manual loans in totals, year filter.
- FLIN-241…249 deviations awaiting FLIN.
- Replacement bank logos from PR #122.
TODO(handover): Give FLIN's product team access to the Kumo tracker (work.kumohq.in/flin, FLIN-nnn tickets) and transfer the product specs that are still local-only (e.g. .scratch/debt-discipline/product-questions.md, marked "Blocking Phase 2"). The specs that code cites are now in specs/; the rest are listed in handover.md#not-in-git.
13. Roadmap signals in the repo
- Unmerged branches (none of this is shipped):
onfido-poc, 2 commits ahead and 436 behind: the Onfido KYC trial from 2026-07-30 (verdict "go"). Marked never to merge; its config plugin is meant to be reused.
- Multi-country roadmap (multi-country.md §5):
- Phase 1b open items: money hydrated from
config.get, onboarding routing fully fromconfig.get, and removal of the deprecated API fields once old builds age out. - Phase 2 (PH): copy overlay, real adapters, and the open infra items.
- Phase 1b open items: money hydrated from
- PH go-live checklist:
- Accept the Bedrock agreement.
- Raise the EC2 quota for the DB tunnel.
- Create the PH Trigger.dev project.
- Replace the placeholder secrets.
- Register the PH Xendit and GHL webhooks.
- Flip
APP_ENV=prodonce CIBI and Onfido land. - Set
EXPO_PUBLIC_SERVER_URL_PHin the production EAS profile and ship a store build. - Make the app available in the Philippine storefronts.
- The full list is in deployment.md and mobile-release.md.
- Hidden future-phase UI in Profile: change PIN, payment methods and history, subscription status, and the Debt Discipline upsell.
- Store release: 1.2.0 is bumped but not built for the stores. It carries the app changes merged since 1.1.0 (for example FLIN-250 and FLIN-136 consultations), and Debt Discipline stays hidden in it unless the production
EXPO_PUBLIC_DEBT_DISCIPLINEchanges.