feat: Organization address and branding patch endpoint

This commit is contained in:
Ahmed Bouhuolia
2024-09-28 17:43:47 +02:00
parent c5d7a2bfd8
commit ca162206a3
10 changed files with 173 additions and 27 deletions

View File

@@ -18,7 +18,7 @@ import BaseController from '@/api/controllers/BaseController';
@Service() @Service()
export default class OrganizationController extends BaseController { export default class OrganizationController extends BaseController {
@Inject() @Inject()
organizationService: OrganizationService; private organizationService: OrganizationService;
/** /**
* Router constructor. * Router constructor.
@@ -69,6 +69,19 @@ export default class OrganizationController extends BaseController {
check('fiscal_year').exists().isIn(MONTHS), check('fiscal_year').exists().isIn(MONTHS),
check('language').exists().isString().isIn(ACCEPTED_LOCALES), check('language').exists().isString().isIn(ACCEPTED_LOCALES),
check('date_format').optional().isIn(DATE_FORMATS), check('date_format').optional().isIn(DATE_FORMATS),
// # Address
check('address').optional({ nullable: true }),
check('address.address_1').optional().isString().trim(),
check('address.address_2').optional().isString().trim(),
check('address.postal_code').optional().isString().trim(),
check('address.city').optional().isString().trim(),
check('address.state_province').optional().isString().trim(),
check('address.phone').optional().isString().trim(),
// # Branding
check('primary_color').optional().isString().trim(),
check('company_logo_key').optional().isString().trim(),
]; ];
} }
@@ -86,7 +99,28 @@ export default class OrganizationController extends BaseController {
*/ */
private get updateOrganizationValidationSchema(): ValidationChain[] { private get updateOrganizationValidationSchema(): ValidationChain[] {
return [ return [
...this.commonOrganizationValidationSchema, // # Profile
check('name').optional().trim(),
check('industry').optional({ nullable: true }).isString().trim(),
check('location').optional().isString().isISO31661Alpha2(),
check('base_currency').optional().isISO4217(),
check('timezone').optional().isIn(moment.tz.names()),
check('fiscal_year').optional().isIn(MONTHS),
check('language').optional().isString().isIn(ACCEPTED_LOCALES),
check('date_format').optional().isIn(DATE_FORMATS),
// # Address
check('address.address_1').optional().isString().trim(),
check('address.address_2').optional().isString().trim(),
check('address.postal_code').optional().isString().trim(),
check('address.city').optional().isString().trim(),
check('address.state_province').optional().isString().trim(),
check('address.phone').optional().isString().trim(),
// # Branding
check('primary_color').optional().isHexColor().trim(),
check('logo_key').optional().isString().trim(),
check('tax_number').optional({ nullable: true }).isString().trim(), check('tax_number').optional({ nullable: true }).isString().trim(),
]; ];
} }
@@ -156,7 +190,7 @@ export default class OrganizationController extends BaseController {
next: NextFunction next: NextFunction
) { ) {
const { tenantId } = req; const { tenantId } = req;
const tenantDTO = this.matchedBodyData(req); const tenantDTO = this.matchedBodyData(req, { includeOptionals: false });
try { try {
await this.organizationService.updateOrganization(tenantId, tenantDTO); await this.organizationService.updateOrganization(tenantId, tenantDTO);

View File

@@ -18,14 +18,26 @@ export interface IOrganizationBuildDTO {
dateFormat?: string; dateFormat?: string;
} }
interface OrganizationAddressDTO {
address1: string;
address2: string;
postalCode: string;
city: string;
stateProvince: string;
phone: string;
}
export interface IOrganizationUpdateDTO { export interface IOrganizationUpdateDTO {
name: string; name: string;
location: string; location?: string;
baseCurrency: string; baseCurrency?: string;
timezone: string; timezone?: string;
fiscalYear: string; fiscalYear?: string;
industry: string; industry?: string;
taxNumber: string; taxNumber?: string;
primaryColor?: string;
logoKey?: string;
address?: OrganizationAddressDTO;
} }
export interface IOrganizationBuildEventPayload { export interface IOrganizationBuildEventPayload {
@@ -36,4 +48,4 @@ export interface IOrganizationBuildEventPayload {
export interface IOrganizationBuiltEventPayload { export interface IOrganizationBuiltEventPayload {
tenantId: number; tenantId: number;
} }

View File

@@ -93,7 +93,7 @@ export default class OrganizationService {
// Triggers the organization built event. // Triggers the organization built event.
await this.eventPublisher.emitAsync(events.organization.built, { await this.eventPublisher.emitAsync(events.organization.built, {
tenantId: tenant.id, tenantId: tenant.id,
} as IOrganizationBuiltEventPayload) } as IOrganizationBuiltEventPayload);
} }
/** /**
@@ -190,11 +190,13 @@ export default class OrganizationService {
this.throwIfTenantNotExists(tenant); this.throwIfTenantNotExists(tenant);
// Validate organization transactions before mutate base currency. // Validate organization transactions before mutate base currency.
await this.validateMutateBaseCurrency( if (organizationDTO.baseCurrency) {
tenant, await this.validateMutateBaseCurrency(
organizationDTO.baseCurrency, tenant,
tenant.metadata?.baseCurrency organizationDTO.baseCurrency,
); tenant.metadata?.baseCurrency
);
}
await tenant.saveMetadata(organizationDTO); await tenant.saveMetadata(organizationDTO);
if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) { if (organizationDTO.baseCurrency !== tenant.metadata?.baseCurrency) {

View File

@@ -13,16 +13,13 @@ import TenantsManagerService from '@/services/Tenancy/TenantsManager';
@Service() @Service()
export default class OrganizationUpgrade { export default class OrganizationUpgrade {
@Inject() @Inject()
tenancy: HasTenancyService; private organizationService: OrganizationService;
@Inject() @Inject()
organizationService: OrganizationService; private tenantsManager: TenantsManagerService;
@Inject()
tenantsManager: TenantsManagerService;
@Inject('agenda') @Inject('agenda')
agenda: any; private agenda: any;
/** /**
* Upgrades the given organization database. * Upgrades the given organization database.
@@ -102,4 +99,4 @@ export default class OrganizationUpgrade {
throw new ServiceError(ERRORS.TENANT_UPGRADE_IS_RUNNING); throw new ServiceError(ERRORS.TENANT_UPGRADE_IS_RUNNING);
} }
} }
} }

View File

@@ -0,0 +1,23 @@
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.up = function (knex) {
return knex.schema.table('tenants_metadata', (table) => {
table.string('primary_color');
table.string('logo_key');
table.json('address');
});
};
/**
* @param { import("knex").Knex } knex
* @returns { Promise<void> }
*/
exports.down = function (knex) {
return knex.schema.table('tenants_metadata', (table) => {
table.dropColumn('primary_color');
table.dropColumn('logo_key');
table.dropColumn('address');
});
};

View File

@@ -169,7 +169,7 @@ export default class Tenant extends BaseModel {
*/ */
static async saveMetadata(tenantId, metadata) { static async saveMetadata(tenantId, metadata) {
const foundMetadata = await TenantMetadata.query().findOne({ tenantId }); const foundMetadata = await TenantMetadata.query().findOne({ tenantId });
const updateOrInsert = foundMetadata ? 'update' : 'insert'; const updateOrInsert = foundMetadata ? 'patch' : 'insert';
return TenantMetadata.query() return TenantMetadata.query()
[updateOrInsert]({ [updateOrInsert]({

View File

@@ -1,8 +1,39 @@
import BaseModel from 'models/Model'; import BaseModel from 'models/Model';
export default class TenantMetadata extends BaseModel { export default class TenantMetadata extends BaseModel {
baseCurrency: string; baseCurrency!: string;
name: string; name!: string;
tenantId!: number;
industry!: string;
location!: string;
language!: string;
timezone!: string;
dateFormat!: string;
fiscalYear!: string;
primaryColor!: string;
logoKey!: string;
address!: object;
static get jsonSchema() {
return {
type: 'object',
required: ['tenantId', 'name', 'baseCurrency'],
properties: {
tenantId: { type: 'integer' },
name: { type: 'string', maxLength: 255 },
industry: { type: 'string', maxLength: 255 },
location: { type: 'string', maxLength: 255 },
baseCurrency: { type: 'string', maxLength: 3 },
language: { type: 'string', maxLength: 255 },
timezone: { type: 'string', maxLength: 255 },
dateFormat: { type: 'string', maxLength: 255 },
fiscalYear: { type: 'string', maxLength: 255 },
primaryColor: { type: 'string', maxLength: 7 }, // Assuming hex color code
logoKey: { type: 'string', maxLength: 255 },
address: { type: 'object' },
},
};
}
/** /**
* Table name. * Table name.

View File

@@ -8,7 +8,7 @@ import styles from './PreferencesBranding.module.scss';
export function PreferencesBrandingFormContent() { export function PreferencesBrandingFormContent() {
return ( return (
<Stack style={{ flex: '1' }}> <Stack style={{ flex: '1' }} spacing={10}>
<FFormGroup name={'companyLogo'} label={'Company Logo'}> <FFormGroup name={'companyLogo'} label={'Company Logo'}>
<Group spacing={15} align={'left'}> <Group spacing={15} align={'left'}>
<BrandingCompanyLogoUpload /> <BrandingCompanyLogoUpload />

View File

@@ -14,6 +14,8 @@ import {
FFormGroup, FFormGroup,
FInputGroup, FInputGroup,
FSelect, FSelect,
Stack,
Group,
} from '@/components'; } from '@/components';
import { inputIntent } from '@/utils'; import { inputIntent } from '@/utils';
import { CLASSES } from '@/constants/classes'; import { CLASSES } from '@/constants/classes';
@@ -99,6 +101,50 @@ export default function PreferencesGeneralForm({ isSubmitting }) {
/> />
</FFormGroup> </FFormGroup>
{/* ---------- Address ---------- */}
<FFormGroup
name={'address'}
label={'Organization Address'}
inline
fastField
>
<Stack>
<FInputGroup
name={'address.address_1'}
placeholder={'Address 1'}
fastField
/>
<FInputGroup
name={'address.address_2'}
placeholder={'Address 2'}
fastField
/>
<Group spacing={15}>
<FInputGroup name={'address.city'} placeholder={'City'} fastField />
<FInputGroup
name={'address.postal_code'}
placeholder={'ZIP Code'}
fastField
/>
</Group>
<Group spacing={15}>
<FInputGroup
name={'address.state_province'}
placeholder={'State or Province'}
fastField
/>
<FInputGroup
name={'address.phone'}
placeholder={'Phone number'}
fastField
/>
</Group>
</Stack>
</FFormGroup>
{/* ---------- Base currency ---------- */} {/* ---------- Base currency ---------- */}
<FFormGroup <FFormGroup
name={'base_currency'} name={'base_currency'}

View File

@@ -24,6 +24,7 @@ const defaultValues = {
date_format: '', date_format: '',
timezone: '', timezone: '',
tax_number: '', tax_number: '',
address: {},
}; };
/** /**