first commit
This commit is contained in:
25
src/features/settings/services/mapSession.ts
Normal file
25
src/features/settings/services/mapSession.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
55
src/features/settings/services/settingsService.ts
Normal file
55
src/features/settings/services/settingsService.ts
Normal 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};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user