Files
waseel/lib/pricing.ts
T
KrikoriosandClaude Opus 5 8807ff41c5 Waseel: driver capture, chat/calls, dispatch, and session fixes
Driver onboarding now photographs the licence, ID card and vehicle
registration and reads the credential fields off them, plus a camera-only
profile selfie riders check the arriving driver against. Adds in-app chat
and WebRTC calls, push-backed ride offers, ratings, cancellation and
payment sheets, settlement, and the owner dashboard endpoints behind them.

Camera permission on Android:
  - Declare CAMERA and READ_MEDIA_IMAGES in the manifest. expo-image-picker's
    own plugin never declares CAMERA, and Android denies a request for an
    undeclared permission instantly and silently — no dialog is ever shown,
    which is indistinguishable from the app not asking at all.
  - Handle canAskAgain: once Android stops showing the dialog, repeating why
    we need it is a dead end, so offer Open Settings instead (lib/capture-
    permission.ts), matching what the location flow already did.

Session: a 401 on a request that carried a token now ends the session
instead of being reinterpreted per-screen — driver-home had been reading it
as "this user has no driver profile" and showing an onboarding form to an
already-onboarded driver. Requests without a token are exempt so a failed
sign-in doesn't sign you out, and the notification is latched per token so
concurrent polls tear the session down once. (root) gains the auth guard
that turns that into the sign-in screen; app/index.tsx only guarded the way
in, leaving a session that ended mid-screen with nowhere to go.

Also ignore .uploads/ — it holds driver licence, ID and vehicle scans plus
profile photos, which are personal data and must not be committed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-26 02:17:55 +03:00

91 lines
3.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Fare model tuned to the Lebanese market:
// - Distance-based with a base fare (like CTaxi/local taxis), not time-only.
// - Typical Beirut rides land in the $38 range riders expect.
// - Prices are quoted in USD (the de facto ride-hailing currency) with an
// L.B.P. equivalent shown for cash settlement.
import { DEFAULT_SERVICE, SERVICES, type ServiceId } from "@/constants/services";
import { tr } from "@/lib/i18n";
export const FARE = {
base: 1.5, // USD, flag drop
perKm: 0.55, // USD per kilometer of the trip
perMin: 0.15, // USD per minute of the trip
minimum: 3.0, // USD minimum fare
} as const;
// Parallel market rate used for the L.B.P. cash equivalent shown in-app.
export const LBP_RATE = 89500;
/**
* The platform's cut of each completed fare.
*
* Until this existed, the driver's earnings and the rider's fare were the same
* number: the dashboard summed `fare_price` and called it "Today's earnings",
* so a rider reading their receipt was reading the driver's revenue, and the
* company's own books had no line of its own. Splitting the fare is what makes
* "what the driver keeps" and "what the company earns" separate, answerable
* questions.
*/
export const COMMISSION_RATE = 0.2;
export type FareSplit = {
/** What the rider pays. */
fareCents: number;
/** What the platform keeps. */
platformFeeCents: number;
/** What the driver is owed. */
driverPayoutCents: number;
/** The rate this split was computed at. */
rate: number;
};
/**
* Split a fare into the driver's payout and the platform's fee.
*
* The fee is rounded and the payout is the remainder, so the two always add
* back up to exactly the fare — computing both by multiplication would let a
* rounding cent go missing or be paid twice, which is the kind of discrepancy
* that surfaces months later as an unreconcilable ledger.
*/
export const splitFare = (
fareCents: number,
rate: number = COMMISSION_RATE,
): FareSplit => {
const fare = Math.max(0, Math.round(fareCents));
const platformFeeCents = Math.round(fare * rate);
return {
fareCents: fare,
platformFeeCents,
driverPayoutCents: fare - platformFeeCents,
rate,
};
};
export const calculateFare = (
{
distanceMeters,
durationSeconds,
}: {
distanceMeters: number;
durationSeconds: number;
},
service: ServiceId = DEFAULT_SERVICE,
): string => {
const km = distanceMeters / 1000;
const minutes = durationSeconds / 60;
const multiplier =
SERVICES.find((s) => s.id === service)?.fareMultiplier ?? 1;
const fare = (FARE.base + km * FARE.perKm + minutes * FARE.perMin) * multiplier;
// The minimum fare is a floor on the final amount.
return Math.max(fare, FARE.minimum).toFixed(2);
};
// Rounds to the nearest 1,000 L.B.P. — the smallest practical cash note.
// The currency suffix is localized (" L.B.P." / " ل.ل." / " LBP").
export const formatLBP = (usd: number): string =>
`${(Math.round((usd * LBP_RATE) / 1000) * 1000).toLocaleString("en-US")}${tr("units.lbpSuffix")}`;