first commit

This commit is contained in:
2026-09-25 16:31:10 +05:30
parent e43cebb43e
commit 88f658fba9
378 changed files with 33242 additions and 115 deletions

View File

@@ -0,0 +1,25 @@
import type {ApiDeviceSession} from '@/services/api/types';
/** A signed-in device, as the settings screen consumes it. */
export interface DeviceSession {
id: string;
device: string;
createdAt: string;
lastUsedAt: string;
expiresAt: string;
/** The browser reading this page. Warn before signing it out. */
isCurrent: boolean;
}
export function toDeviceSession(s: ApiDeviceSession): DeviceSession {
return {
id: s.id,
// The platform records whatever the client sent at sign-in. An empty label
// is possible, and "Unknown device" is more honest than a blank row.
device: s.device || 'Unknown device',
createdAt: s.created_at,
lastUsedAt: s.last_used_at,
expiresAt: s.expires_at,
isCurrent: s.current,
};
}

View File

@@ -0,0 +1,55 @@
import {settingsRepository} from '@/features/settings/repositories/settingsRepository';
import type {MerchantProfile} from '@/features/settings/types/settings';
/**
* Domain rules for the settings module.
*
* The validation lives here rather than in the form for the same reason the
* login rules live in authService: a rule that only exists inside a component
* cannot be reused by a second screen, cannot be unit-tested without mounting
* React, and quietly disappears the day the screen is redesigned.
*/
export type ProfileFieldErrors = Partial<Record<keyof MerchantProfile, string>>;
export function validateProfile(profile: MerchantProfile): ProfileFieldErrors {
const errors: ProfileFieldErrors = {};
if (!profile.businessName.trim()) {
errors.businessName = 'Business name is required';
}
if (!/^\S+@\S+\.\S+$/.test(profile.email)) {
errors.email = 'Enter a valid email address';
}
// Optional, but a partial GSTIN is worse than none — it will fail at
// settlement time rather than here.
if (profile.gstin && profile.gstin.length !== 15) {
errors.gstin = 'GSTIN is 15 characters';
}
return errors;
}
export const settingsService = {
validateProfile,
/**
* Validate, then save. Returns the field errors rather than throwing —
* invalid input is an expected outcome of a form submission, not an
* exception, and the caller has to render them either way.
*/
async saveProfile(
profile: MerchantProfile,
): Promise<{ok: true} | {ok: false; errors: ProfileFieldErrors; message?: string}> {
const errors = validateProfile(profile);
if (Object.keys(errors).length > 0) return {ok: false, errors};
const res = await settingsRepository.updateProfile(profile);
if (!res.ok) {
return {
ok: false,
errors: {},
message: res.message ?? 'Could not save profile',
};
}
return {ok: true};
},
};