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()
export default class OrganizationController extends BaseController {
@Inject()
organizationService: OrganizationService;
private organizationService: OrganizationService;
/**
* Router constructor.
@@ -69,6 +69,19 @@ export default class OrganizationController extends BaseController {
check('fiscal_year').exists().isIn(MONTHS),
check('language').exists().isString().isIn(ACCEPTED_LOCALES),
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[] {
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(),
];
}
@@ -156,7 +190,7 @@ export default class OrganizationController extends BaseController {
next: NextFunction
) {
const { tenantId } = req;
const tenantDTO = this.matchedBodyData(req);
const tenantDTO = this.matchedBodyData(req, { includeOptionals: false });
try {
await this.organizationService.updateOrganization(tenantId, tenantDTO);

View File

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

View File

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

View File

@@ -13,16 +13,13 @@ import TenantsManagerService from '@/services/Tenancy/TenantsManager';
@Service()
export default class OrganizationUpgrade {
@Inject()
tenancy: HasTenancyService;
private organizationService: OrganizationService;
@Inject()
organizationService: OrganizationService;
@Inject()
tenantsManager: TenantsManagerService;
private tenantsManager: TenantsManagerService;
@Inject('agenda')
agenda: any;
private agenda: any;
/**
* Upgrades the given organization database.
@@ -102,4 +99,4 @@ export default class OrganizationUpgrade {
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) {
const foundMetadata = await TenantMetadata.query().findOne({ tenantId });
const updateOrInsert = foundMetadata ? 'update' : 'insert';
const updateOrInsert = foundMetadata ? 'patch' : 'insert';
return TenantMetadata.query()
[updateOrInsert]({

View File

@@ -1,8 +1,39 @@
import BaseModel from 'models/Model';
export default class TenantMetadata extends BaseModel {
baseCurrency: string;
name: string;
baseCurrency!: 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.

View File

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

View File

@@ -14,6 +14,8 @@ import {
FFormGroup,
FInputGroup,
FSelect,
Stack,
Group,
} from '@/components';
import { inputIntent } from '@/utils';
import { CLASSES } from '@/constants/classes';
@@ -99,6 +101,50 @@ export default function PreferencesGeneralForm({ isSubmitting }) {
/>
</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 ---------- */}
<FFormGroup
name={'base_currency'}

View File

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