- Added services and use cases for managing account details, including changing passwords and updating profiles. - Introduced error handling for duplicate emails and password mismatches. - Created controllers for handling account-related requests in the Express framework. - Updated API routes to include new account management endpoints. - Enhanced the account profile schema to support email updates. - Implemented tests for account management functionalities to ensure reliability and correctness.
272 lines
6.3 KiB
TypeScript
272 lines
6.3 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
|
|
import bcrypt from "bcrypt";
|
|
import type { QueryInterface, Sequelize } from "sequelize";
|
|
import { QueryTypes } from "sequelize";
|
|
|
|
type CreateAccountParams = {
|
|
id?: string;
|
|
email: string;
|
|
password: string;
|
|
name?: string;
|
|
status?: "active" | "disabled";
|
|
languageCode?: string;
|
|
};
|
|
|
|
type CreateCompanyParams = {
|
|
id?: string;
|
|
legalName: string;
|
|
tradeName?: string | null;
|
|
tin?: string | null;
|
|
slug?: string;
|
|
email?: string | null;
|
|
phone?: string | null;
|
|
website?: string | null;
|
|
defaultPaymentTermId?: string | null;
|
|
status?: "active" | "disabled";
|
|
};
|
|
|
|
type CreateMembershipParams = {
|
|
id?: string;
|
|
accountId: string;
|
|
companyId: string;
|
|
status?: "active" | "disabled" | "invited";
|
|
};
|
|
|
|
type CreatePaymentTermParams = {
|
|
id?: string;
|
|
companyId: string;
|
|
name: string;
|
|
description?: string | null;
|
|
isActive?: boolean;
|
|
isSystem?: boolean;
|
|
dueRules?: Array<{
|
|
dueDays: number;
|
|
percentageValue: number;
|
|
percentageScale?: number;
|
|
}>;
|
|
};
|
|
|
|
export async function createTestAccount(database: Sequelize, params: CreateAccountParams) {
|
|
const queryInterface = database.getQueryInterface();
|
|
const now = new Date();
|
|
const id = params.id ?? randomUUID();
|
|
const passwordHash = await bcrypt.hash(params.password, 10);
|
|
|
|
await queryInterface.bulkInsert("identity_accounts", [
|
|
{
|
|
id,
|
|
email: params.email,
|
|
password_hash: passwordHash,
|
|
name: params.name ?? "Test Account",
|
|
avatar_url: null,
|
|
language_code: params.languageCode ?? "es",
|
|
status: params.status ?? "active",
|
|
created_at: now,
|
|
updated_at: now,
|
|
},
|
|
]);
|
|
|
|
return {
|
|
id,
|
|
email: params.email,
|
|
password: params.password,
|
|
};
|
|
}
|
|
|
|
export async function createTestCompany(database: Sequelize, params: CreateCompanyParams) {
|
|
const queryInterface = database.getQueryInterface();
|
|
const now = new Date();
|
|
const id = params.id ?? randomUUID();
|
|
|
|
await queryInterface.bulkInsert("companies", [
|
|
{
|
|
id,
|
|
legal_name: params.legalName,
|
|
trade_name: params.tradeName ?? null,
|
|
tin: params.tin ?? null,
|
|
slug: params.slug ?? `company-${id.slice(0, 8)}`,
|
|
email: params.email ?? null,
|
|
phone: params.phone ?? null,
|
|
website: params.website ?? null,
|
|
default_payment_term_id: params.defaultPaymentTermId ?? null,
|
|
status: params.status ?? "active",
|
|
created_at: now,
|
|
updated_at: now,
|
|
},
|
|
]);
|
|
|
|
return {
|
|
id,
|
|
slug: params.slug ?? `company-${id.slice(0, 8)}`,
|
|
};
|
|
}
|
|
|
|
export async function createTestMembership(database: Sequelize, params: CreateMembershipParams) {
|
|
const queryInterface = database.getQueryInterface();
|
|
const now = new Date();
|
|
const id = params.id ?? randomUUID();
|
|
|
|
await queryInterface.bulkInsert("identity_company_memberships", [
|
|
{
|
|
id,
|
|
account_id: params.accountId,
|
|
company_id: params.companyId,
|
|
status: params.status ?? "active",
|
|
created_at: now,
|
|
updated_at: now,
|
|
},
|
|
]);
|
|
|
|
return {
|
|
id,
|
|
};
|
|
}
|
|
|
|
export async function createTestPaymentTerm(database: Sequelize, params: CreatePaymentTermParams) {
|
|
const queryInterface = database.getQueryInterface();
|
|
const now = new Date();
|
|
const id = params.id ?? randomUUID();
|
|
const dueRules = params.dueRules ?? [
|
|
{
|
|
dueDays: 30,
|
|
percentageValue: 10000,
|
|
percentageScale: 2,
|
|
},
|
|
];
|
|
|
|
await queryInterface.bulkInsert("payment_terms", [
|
|
{
|
|
id,
|
|
company_id: params.companyId,
|
|
name: params.name,
|
|
description: params.description ?? null,
|
|
is_active: params.isActive ?? true,
|
|
is_system: params.isSystem ?? false,
|
|
created_at: now,
|
|
updated_at: now,
|
|
deleted_at: null,
|
|
},
|
|
]);
|
|
|
|
await queryInterface.bulkInsert(
|
|
"payment_term_due_rules",
|
|
dueRules.map((rule) => ({
|
|
due_id: randomUUID(),
|
|
payment_term_id: id,
|
|
due_days: rule.dueDays,
|
|
percentage_value: rule.percentageValue,
|
|
percentage_scale: rule.percentageScale ?? 2,
|
|
created_at: now,
|
|
updated_at: now,
|
|
}))
|
|
);
|
|
|
|
return {
|
|
id,
|
|
};
|
|
}
|
|
|
|
export async function findCompanyRow(database: Sequelize, companyId: string) {
|
|
const rows = await database.query<{
|
|
id: string;
|
|
legal_name: string;
|
|
trade_name: string | null;
|
|
tin: string | null;
|
|
slug: string;
|
|
email: string | null;
|
|
phone: string | null;
|
|
website: string | null;
|
|
default_payment_term_id: string | null;
|
|
status: string;
|
|
}>("SELECT * FROM companies WHERE id = :companyId", {
|
|
replacements: { companyId },
|
|
type: QueryTypes.SELECT,
|
|
});
|
|
|
|
return rows[0] ?? null;
|
|
}
|
|
|
|
export async function findAccountRow(database: Sequelize, accountId: string) {
|
|
const rows = await database.query<{
|
|
id: string;
|
|
email: string;
|
|
password_hash: string;
|
|
name: string;
|
|
avatar_url: string | null;
|
|
language_code: string;
|
|
status: string;
|
|
}>("SELECT * FROM identity_accounts WHERE id = :accountId", {
|
|
replacements: { accountId },
|
|
type: QueryTypes.SELECT,
|
|
});
|
|
|
|
return rows[0] ?? null;
|
|
}
|
|
|
|
export async function updateAccountStatus(
|
|
database: Sequelize,
|
|
params: {
|
|
accountId: string;
|
|
status: "active" | "disabled" | "pending_verification";
|
|
}
|
|
) {
|
|
const queryInterface = database.getQueryInterface();
|
|
|
|
await queryInterface.bulkUpdate(
|
|
"identity_accounts",
|
|
{
|
|
status: params.status,
|
|
updated_at: new Date(),
|
|
},
|
|
{
|
|
id: params.accountId,
|
|
}
|
|
);
|
|
}
|
|
|
|
export async function loginByApi(params: {
|
|
request: (
|
|
path: string,
|
|
init?: RequestInit
|
|
) => Promise<{
|
|
status: number;
|
|
headers: Headers;
|
|
body: unknown;
|
|
}>;
|
|
email: string;
|
|
password: string;
|
|
}) {
|
|
return params.request("/api/v1/identity/auth/login", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
email: params.email,
|
|
password: params.password,
|
|
}),
|
|
});
|
|
}
|
|
|
|
export function buildCompanyHeaders(params: {
|
|
accessToken?: string;
|
|
companyId?: string;
|
|
}): Record<string, string> {
|
|
const headers: Record<string, string> = {};
|
|
|
|
if (params.accessToken) {
|
|
headers.authorization = `Bearer ${params.accessToken}`;
|
|
}
|
|
|
|
if (params.companyId) {
|
|
headers["x-company-id"] = params.companyId;
|
|
}
|
|
|
|
return headers;
|
|
}
|
|
|
|
export function getQueryInterface(database: Sequelize): QueryInterface {
|
|
return database.getQueryInterface();
|
|
}
|