# BankIslami integration guide

Four touch points: the encrypted hand-off into the journey, customer registration, the daily
lead extract out to the bank, and the two result files back from the bank.

---

## 1. Hand-off link

BankIslami open the React journey inside a WebView and append the customer's details, encrypted
with **our public key**:

```
https://bipl.efulife.com/?data=<url-encoded ciphertext>
```

### Keys

We generate the keypair and send BankIslami **only the public half**. The private key never
leaves the EFU server. That means a compromise on the bank's side cannot be used to read a
payload or forge one.

```bash
php artisan bipl:keypair                # generate; prints the public key to send
php artisan bipl:keypair --print-public # print it again later
```

| Setting | Default | Notes |
|---|---|---|
| Algorithm | RSA 2048 | `--bits=4096` if BIPL require it |
| Padding | **OAEP** (SHA-1) | `BIPL_RSA_PADDING=pkcs1` if their stack cannot do OAEP |
| Encoding | base64 | `BIPL_PAYLOAD_ENCODING=hex` if they prefer hex |
| Max plaintext | 214 bytes per block at 2048/OAEP | Longer payloads are encrypted block by block and concatenated |

> The private key lives at `storage/keys/bipl-private.pem`, which is git-ignored. **Back it up.**
> Losing it means every link the bank has issued stops working.

### Payload shape

Either shape works — set `BIPL_PAYLOAD_MODE` to `blob`, `fields`, or leave it on `auto`.

**A — one encrypted blob** (`?data=<enc>`), carrying JSON or a form-encoded string:

```json
{
  "msisdn":        "0300-1234567",
  "cnic":          "42101-1234567-1",
  "name":          "Ayesha Khan",
  "email":         "ayesha@example.com",
  "dob":           "1990-05-14",
  "gender":        "F",
  "city":          "Karachi",
  "account_no":    "PK36BKIP0000001234567890",
  "account_title": "Ayesha Khan",
  "branch_code":   "0123",
  "customer_id":   "BIPL000123",
  "ts":            1785435441
}
```

**B — one encrypted value per parameter:**

```
?msisdn=<enc>&cnic=<enc>&name=<enc>&email=<enc>
```

Each value is encrypted separately, so each stays inside a single RSA block. If BIPL send some
parameters in the clear, list them in `BIPL_PLAINTEXT_FIELDS` (comma separated) and they will be
passed through undecrypted.

**`msisdn` is the required field.** It is how a returning customer is recognised. Everything else
is optional but improves the experience: without `dob` we cannot check the 18–64 enrolment band up
front, and without `name` the greeting is generic.

### Field names are configurable

`config/bipl.php → payload_map` holds accepted aliases for every field — `msisdn` / `mobile` /
`mobile_no` / `cell` all resolve to the same thing, and nested objects are flattened. A rename on
the bank's side is a config change, not a release.

### Value handling

| Field | Accepted | Stored as |
|---|---|---|
| `msisdn` | `03001234567`, `+923001234567`, `0092300…`, with spaces or dashes | `03001234567`, encrypted |
| `cnic` | With or without dashes | 13 digits, encrypted |
| `account_no` | IBAN or account number, any punctuation | Alphanumeric uppercase, encrypted |
| `dob` | `Y-m-d`, `d-m-Y`, `d/m/Y`, `d-M-Y`, `Ymd`, `m/d/Y` | `Y-m-d` |
| `gender` | Anything starting `m` / `f` | `male` / `female` |
| `ts` | Unix seconds or milliseconds, or a parseable date | Checked against `BIPL_PAYLOAD_TTL` |

### Reference implementation (the bank's side)

```php
$publicKey = openssl_pkey_get_public(file_get_contents('efu-public.pem'));

openssl_public_encrypt(
    json_encode($payload),
    $ciphertext,
    $publicKey,
    OPENSSL_PKCS1_OAEP_PADDING,
);

$url = 'https://bipl.efulife.com/?data=' . urlencode(base64_encode($ciphertext));
```

Java (`RSA/ECB/OAEPWithSHA-1AndMGF1Padding`) and .NET (`RSA.Encrypt(data, RSAEncryptionPadding.OaepSHA1)`)
produce identical output.

> **Note on `+`.** Base64 contains `+`, which decodes to a space in a query string. The backend
> repairs this automatically, but please URL-encode the value at source.

To produce a sample link locally:

```bash
php artisan bipl:make-link                      # blob mode
php artisan bipl:make-link --fields             # per-field mode
php artisan bipl:make-link --msisdn=03211234567 --name="Ali Raza"
```

Every landing — successful or not — is recorded in `customer_sessions` with the failure reason, IP
and user agent, so a report of "the link doesn't work" can be traced immediately.

---

## 2. Registration

A customer BankIslami send us may not exist here yet. The decision is made on **msisdn**:

```
  decrypt payload
        │
        ▼
  msisdn already registered?
        │
   ┌────┴────┐
   │         │
  yes        no
   │         │
   ▼         ▼
 next:     next: "consent"
 products  → consent screen → POST /registration → customer created
 or        
 dashboard 
```

**Nothing about an unregistered visitor is written to the customer table until they accept the
consent.** Their decrypted details are held, encrypted, on the session row only. That means a
customer record existing is itself evidence that consent was given, with the exact wording, IP,
user agent and timestamp stored alongside it in `consents`.

Until registration completes, the session can only reach `/api/v1/registration` and
`/api/v1/session`. Everything else answers **403 `registration_required`**.

Registering does **not** buy cover and deducts nothing. Purchasing a plan is a separate consent,
captured per subscription.

---

## 3. Daily lead extract (EFU → BIPL)

Generated by EFU Operations, or automatically at `LEAD_EXPORT_CUTOFF`. BIPL Operations download it
from **Lead Extracts** in the portal. A lead can only ever appear in one extract.

UTF-8 CSV, comma delimited, header row present:

| Column | Example |
|---|---|
| Subscription No | `SUB-2607-000001` |
| Lead Date | `2026-07-30 14:22:05` |
| Customer ID | `BIPL000123` |
| CNIC | `42101-1234567-1` |
| Customer Name | `Ayesha Khan` |
| Mobile | `03001234567` |
| Email | |
| Date of Birth | `1990-05-14` |
| Gender | `female` |
| City | `Karachi` |
| Account Number | `PK36BKIP0000001234567890` |
| Account Title | `Ayesha Khan` |
| Branch Code | `0123` |
| Product Code | `P1` |
| Product Name | `EFU Afiyah Takaful Plus Plan` |
| Plan | `B` |
| Frequency | `Annual` |
| Contribution (PKR) | `11150.00` |
| Consent Date/Time | `2026-07-30 14:22:05` |
| Consent Channel | `web` |
| Status | `Lead Captured` |

**Subscription No is the key for both return files.** Please carry it through unchanged.

The column set and order are configurable in `config/takaful.php → lead_export_columns`.

---

## 4. CBC result file (BIPL → EFU)

Uploaded at **File Uploads → CBC Result File** once the calls are done.

| Column | Required | Accepted values |
|---|---|---|
| `subscription_no` | Yes* | The reference from the extract |
| `cnic` | Yes* | Alternative to the reference |
| `cbc_status` | Yes | Confirmed / Rejected / Not Reachable |
| `cbc_date` | No | `d/m/Y`, `Y-m-d`, `d-m-Y`, with or without a time |
| `agent` | No | Name of the calling officer |
| `remarks` | No | Free text, stored against the subscription |

\* One of the two. CNIC is only accepted when it matches exactly one open subscription; where a
customer holds several, the subscription reference is required.

```csv
Subscription No.,CBC Status,CBC Date,Agent,Remarks
SUB-2607-000001,Confirmed,30/07/2026,Sana Ahmed,Customer confirmed
SUB-2607-000002,Rejected,30/07/2026,Sana Ahmed,Not interested
SUB-2607-000003,Not Reachable,30/07/2026,Sana Ahmed,Three attempts
```

**Effect**

- *Confirmed* → status becomes `deduction_pending` and the first contribution instalment is raised.
- *Rejected* / *Not Reachable* → status becomes `cbc_rejected` and the lead is closed.

---

## 5. Successful deduction file (BIPL → EFU)

Uploaded at **File Uploads → Successful Deduction File**. **This is what activates the policy.**

| Column | Required | Notes |
|---|---|---|
| `subscription_no` | Yes* | Reference or, once active, the policy number |
| `cnic` | Yes* | Alternative to the reference |
| `amount` | Recommended | Must equal the contribution; the row is rejected if it does not |
| `deduction_date` | No | Defaults to the upload date; drives the cover start date |
| `bank_reference` | No | Transaction id, stored against the instalment |
| `status` | No | Omit for a file of successes; include to report failures in the same file |
| `reason` | No | Failure reason when `status` is negative |

```csv
Subscription No,Amount,Deduction Date,Bank Reference,Status
SUB-2607-000001,11150.00,30/07/2026,TXN99881,Success
SUB-2607-000004,7500.00,30/07/2026,,Failed
```

**Effect on a successful row**

1. The open instalment is settled with the amount, date and bank reference.
2. On the *first* success: a policy number is issued (`BIPL-P1-2607-000001`), the cover start date
   is set to the deduction date plus any product waiting period, the cover end date to one year
   later, and the free look expiry to the deduction date plus 15 days.
3. Status becomes `active`. Later successful rows are treated as renewals.

A failed row sets `deduction_failed`, or `lapsed` if the policy was already active.

### Header matching

Headers are matched case-, space- and punctuation-insensitively, so `Subscription No.`,
`SUBSCRIPTION_NO` and `subscription no` all resolve. Comma, semicolon, pipe and tab delimiters are
detected automatically. Additional aliases can be added in
`config/takaful.php → inbound_columns` without a code change.

### Error handling

Each row is processed in its own transaction. One bad row never blocks the file: the batch is
marked *Partially Processed* and every row's outcome is listed on the batch page, with the reason.
Correct the rejected rows and upload just those. Re-uploading a byte-identical file is refused as a
duplicate.

---

## 6. Suggested go-live checklist

- [ ] Run `php artisan bipl:keypair` on the production server and send BIPL the **public key only**
- [ ] Back the private key up somewhere safe and off the server
- [ ] Confirm the padding with BIPL — OAEP unless their stack forces PKCS#1 v1.5
- [ ] Confirm the payload shape (`blob` or `fields`) and set `BIPL_PAYLOAD_MODE` accordingly
- [ ] Confirm the exact parameter names and update `payload_map` if they differ
- [ ] List any parameters they send unencrypted in `BIPL_PLAINTEXT_FIELDS`
- [ ] Exchange one sample ciphertext each way and confirm both sides decrypt it
- [ ] Set `BIPL_ALLOW_REPLAY=false` and `BIPL_PAYLOAD_TTL` to the agreed window
- [ ] Set `OTP_TEST_MODE=false` and wire `OtpService::dispatch()` to the SMS gateway
- [ ] Add the React app's origin to `FRONTEND_ORIGINS`
- [ ] Agree the daily extract cut-off time (`LEAD_EXPORT_CUTOFF`)
- [ ] Run one full cycle in UAT with a real sample file from the bank
