add server to monorepo.

This commit is contained in:
a.bouhuolia
2023-02-03 11:57:50 +02:00
parent 28e309981b
commit 80b97b5fdc
1303 changed files with 137049 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
IProjectCreatedEventPayload,
IProjectCreateDTO,
IProjectCreatePOJO,
IProjectCreatingEventPayload,
IProjectStatus,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import { ProjectsValidator } from './ProjectsValidator';
import events from '@/subscribers/events';
@Service()
export default class CreateProject {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private validator: ProjectsValidator;
/**
* Creates a new credit note.
* @param {IProjectCreateDTO} creditNoteDTO
*/
public createProject = async (
tenantId: number,
projectDTO: IProjectCreateDTO
): Promise<IProjectCreatePOJO> => {
const { Project } = this.tenancy.models(tenantId);
// Validate customer existance.
await this.validator.validateContactExists(tenantId, projectDTO.contactId);
// Triggers `onProjectCreate` event.
await this.eventPublisher.emitAsync(events.project.onCreate, {
tenantId,
projectDTO,
} as IProjectCreatedEventPayload);
// Creates a new project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onProjectCreating` event.
await this.eventPublisher.emitAsync(events.project.onCreating, {
tenantId,
projectDTO,
trx,
} as IProjectCreatingEventPayload);
// Upsert the project object.
const project = await Project.query(trx).upsertGraph({
...projectDTO,
status: IProjectStatus.InProgress,
});
// Triggers `onProjectCreated` event.
await this.eventPublisher.emitAsync(events.project.onCreated, {
tenantId,
projectDTO,
project,
trx,
} as IProjectCreatedEventPayload);
return project;
});
};
}

View File

@@ -0,0 +1,61 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
IProjectDeletedEventPayload,
IProjectDeleteEventPayload,
IProjectDeletingEventPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export default class DeleteProject {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Deletes the give project.
* @param {number} projectId -
* @returns {Promise<void>}
*/
public deleteProject = async (tenantId: number, projectId: number) => {
const { Project } = this.tenancy.models(tenantId);
// Triggers `onProjectDelete` event.
await this.eventPublisher.emitAsync(events.project.onDelete, {
tenantId,
projectId,
} as IProjectDeleteEventPayload);
// Validate customer existance.
const oldProject = await Project.query().findById(projectId).throwIfNotFound();
// Deletes the given project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onProjectDeleting` event.
await this.eventPublisher.emitAsync(events.project.onDeleting, {
tenantId,
oldProject,
trx,
} as IProjectDeletingEventPayload);
// Deletes the project from the storage.
await Project.query(trx).findById(projectId).delete();
// Triggers `onProjectDeleted` event.
await this.eventPublisher.emitAsync(events.project.onDeleted, {
tenantId,
oldProject,
trx,
} as IProjectDeletedEventPayload);
});
};
}

View File

@@ -0,0 +1,87 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
IProjectEditDTO,
IProjectEditedEventPayload,
IProjectEditEventPayload,
IProjectEditingEventPayload,
IProjectEditPOJO,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
import { ProjectsValidator } from './ProjectsValidator';
@Service()
export default class EditProjectService {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
@Inject()
private projectsValidator: ProjectsValidator;
/**
* Edits a new credit note.
* @param {number} tenantId -
* @param {number} projectId -
* @param {IProjectEditDTO} projectDTO -
*/
public editProject = async (
tenantId: number,
projectId: number,
projectDTO: IProjectEditDTO
): Promise<IProjectEditPOJO> => {
const { Project } = this.tenancy.models(tenantId);
// Validate customer existance.
const oldProject = await Project.query().findById(projectId).throwIfNotFound();
// Validate the project's contact id existance.
if (oldProject.contactId !== projectDTO.contactId) {
await this.projectsValidator.validateContactExists(
tenantId,
projectDTO.contactId
);
}
// Triggers `onProjectEdit` event.
await this.eventPublisher.emitAsync(events.project.onEdit, {
tenantId,
oldProject,
projectDTO,
} as IProjectEditEventPayload);
// Edits the given project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onProjectEditing` event.
await this.eventPublisher.emitAsync(events.project.onEditing, {
tenantId,
projectDTO,
oldProject,
trx,
} as IProjectEditingEventPayload);
// Upsert the project object.
const project = await Project.query(trx).upsertGraph({
id: projectId,
...projectDTO,
});
// Triggers `onProjectEdited` event.
await this.eventPublisher.emitAsync(events.project.onEdited, {
tenantId,
oldProject,
project,
projectDTO,
trx,
} as IProjectEditedEventPayload);
return project;
});
};
}

View File

@@ -0,0 +1,46 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import { IProjectStatus } from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
@Service()
export default class EditProjectStatusService {
@Inject()
uow: UnitOfWork;
@Inject()
tenancy: HasTenancyService;
@Inject()
eventPublisher: EventPublisher;
/**
* Edits a new credit note.
* @param {number} projectId -
* @param {IProjectStatus} status -
*/
public editProjectStatus = async (
tenantId: number,
projectId: number,
status: IProjectStatus
) => {
const { Project } = this.tenancy.models(tenantId);
// Validate customer existance.
const oldProject = await Project.query()
.findById(projectId)
.throwIfNotFound();
// Edits the given project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Upsert the project object.
const project = await Project.query(trx).upsertGraph({
id: projectId,
status,
});
return project;
});
};
}

View File

@@ -0,0 +1,43 @@
import { IProjectGetPOJO } from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ProjectDetailedTransformer } from './ProjectDetailedTransformer';
@Service()
export default class GetProject {
@Inject()
tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the project.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<IProjectGetPOJO>}
*/
public getProject = async (
tenantId: number,
projectId: number
): Promise<IProjectGetPOJO> => {
const { Project } = this.tenancy.models(tenantId);
// Retrieve the project.
const project = await Project.query()
.findById(projectId)
.withGraphFetched('contact')
.modify('totalExpensesDetails')
.modify('totalBillsDetails')
.modify('totalTasksDetails')
.throwIfNotFound();
// Transformes and returns object.
return this.transformer.transform(
tenantId,
project,
new ProjectDetailedTransformer()
);
};
}

View File

@@ -0,0 +1,139 @@
import { Inject, Service } from 'typedi';
import { flatten, includes, isEmpty } from 'lodash';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { ProjectBillableBillTransformer } from './ProjectBillableBillTransformer';
import { ProjectBillableExpenseTransformer } from './ProjectBillableExpenseTransformer';
import { ProjectBillableTaskTransformer } from './ProjectBillableTaskTransformer';
import {
ProjectBillableEntriesQuery,
ProjectBillableEntry,
ProjectBillableType,
} from '@/interfaces';
import { ProjectBillableGetter } from './_types';
@Service()
export default class GetProjectBillableEntries {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Billable getter with type.
* @get
* @returns {ProjectBillableGetter[]}
*/
get billableGetters(): ProjectBillableGetter[] {
return [
{ type: ProjectBillableType.Task, getter: this.getProjectBillableTasks },
{
type: ProjectBillableType.Expense,
getter: this.getProjectBillableExpenses,
},
{ type: ProjectBillableType.Bill, getter: this.getProjectBillableBills },
];
}
/**
* Retrieve the billable entries of the given project.
* @param {number} tenantId
* @param {number} projectId
* @param {ProjectBillableEntriesQuery} query -
* @returns {}
*/
public getProjectBillableEntries = async (
tenantId: number,
projectId: number,
query: ProjectBillableEntriesQuery = {
billableType: [],
}
): Promise<ProjectBillableEntry[]> => {
const gettersOpers = this.billableGetters
.filter(
(billableGetter) =>
includes(query.billableType, billableGetter.type) ||
isEmpty(query.billableType)
)
.map((billableGetter) =>
billableGetter.getter(tenantId, projectId, query)
);
const gettersResults = await Promise.all(gettersOpers);
return flatten(gettersResults);
};
/**
* Retrieves the billable tasks of the given project.
* @param {number} tenantId
* @param {number} projectId
* @param {ProjectBillableEntriesQuery} query
* @returns {ProjectBillableEntry[]}
*/
private getProjectBillableTasks = async (
tenantId: number,
projectId: number,
query: ProjectBillableEntriesQuery
): Promise<ProjectBillableEntry[]> => {
const { Task } = this.tenancy.models(tenantId);
const billableTasks = await Task.query().where('projectId', projectId);
return this.transformer.transform(
tenantId,
billableTasks,
new ProjectBillableTaskTransformer()
);
};
/**
* Retrieves the billable expenses of the given project.
* @param {number} tenantId
* @param {number} projectId
* @param {ProjectBillableEntriesQuery} query
* @returns
*/
private getProjectBillableExpenses = async (
tenantId: number,
projectId: number,
query: ProjectBillableEntriesQuery
) => {
const { Expense } = this.tenancy.models(tenantId);
const billableExpenses = await Expense.query()
.where('projectId', projectId)
.modify('filterByDateRange', null, query.toDate)
.modify('filterByPublished');
return this.transformer.transform(
tenantId,
billableExpenses,
new ProjectBillableExpenseTransformer()
);
};
/**
* Retrieves billable bills of the given project.
* @param {number} tenantId
* @param {number} projectId
* @param {ProjectBillableEntriesQuery} query
*/
private getProjectBillableBills = async (
tenantId: number,
projectId: number,
query: ProjectBillableEntriesQuery
) => {
const { Bill } = this.tenancy.models(tenantId);
const billableBills = await Bill.query()
.where('projectId', projectId)
.modify('published');
return this.transformer.transform(
tenantId,
billableBills,
new ProjectBillableBillTransformer()
);
};
}

View File

@@ -0,0 +1,38 @@
import { IProjectGetPOJO } from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ProjectDetailedTransformer } from './ProjectDetailedTransformer';
@Service()
export default class GetProjects {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the projects list.
* @param {number} tenantId
* @param {number} creditNoteId
* @returns {Promise<IProjectGetPOJO[]>}
*/
public getProjects = async (tenantId: number): Promise<IProjectGetPOJO[]> => {
const { Project } = this.tenancy.models(tenantId);
// Retrieve projects.
const projects = await Project.query()
.withGraphFetched('contact')
.modify('totalExpensesDetails')
.modify('totalBillsDetails')
.modify('totalTasksDetails');
// Transformes and returns object.
return this.transformer.transform(
tenantId,
projects,
new ProjectDetailedTransformer()
);
};
}

View File

@@ -0,0 +1,45 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class ProjectBillableBill {
@Inject()
private tenancy: HasTenancyService;
/**
* Increase the invoiced amount of the given bill.
* @param {number} tenantId - Tenant id.
* @param {number} billId - Bill id.
* @param {number} invoicedAmount - Invoiced amount.
*/
public increaseInvoicedBill = async (
tenantId: number,
billId: number,
invoicedAmount: number
) => {
const { Bill } = this.tenancy.models(tenantId);
await Bill.query()
.findById(billId)
.increment('projectInvoicedAmount', invoicedAmount);
};
/**
* Decrease the invoiced amount of the given bill.
* @param {number} tenantId
* @param {number} billId - Bill id.
* @param {number} invoiceHours - Invoiced amount.
* @returns {}
*/
public decreaseInvoicedBill = async (
tenantId: number,
billId: number,
invoiceHours: number
) => {
const { Bill } = this.tenancy.models(tenantId);
await Bill.query()
.findById(billId)
.decrement('projectInvoicedAmount', invoiceHours);
};
}

View File

@@ -0,0 +1,116 @@
import async from 'async';
import { Knex } from 'knex';
import { Service } from 'typedi';
import { ISaleInvoice, ISaleInvoiceDTO, ProjectLinkRefType } from '@/interfaces';
import { ProjectBillableExpense } from './ProjectBillableExpense';
import { filterEntriesByRefType } from './_utils';
@Service()
export class ProjectBillableBill {
@Inject()
private projectBillableExpense: ProjectBillableExpense;
/**
* Increases the invoiced amount of the given bills that associated
* to the invoice entries.
* @param {number} tenantId
* @param {ISaleInvoice | ISaleInvoiceDTO} saleInvoiceDTO
* @param {Knex.Transaction} trx
*/
public increaseInvoicedBill = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoice | ISaleInvoiceDTO,
trx?: Knex.Transaction
) => {
// Initiates a new queue for accounts balance mutation.
const saveAccountsBalanceQueue = async.queue(
this.increaseInvoicedExpenseQueue,
10
);
const filteredEntries = filterEntriesByRefType(
saleInvoiceDTO.entries,
ProjectLinkRefType.Task
);
filteredEntries.forEach((entry) => {
saveAccountsBalanceQueue.push({
tenantId,
projectRefId: entry.projectRefId,
projectRefInvoicedAmount: entry.projectRefInvoicedAmount,
trx,
});
});
if (filteredEntries.length > 0) {
await saveAccountsBalanceQueue.drain();
}
};
/**
* Decreases the invoiced amount of the given bills that associated
* to the invoice entries.
* @param {number} tenantId
* @param {ISaleInvoice | ISaleInvoiceDTO} saleInvoiceDTO
* @param {Knex.Transaction} trx
*/
public decreaseInvoicedBill = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoice | ISaleInvoiceDTO,
trx?: Knex.Transaction
) => {
// Initiates a new queue for accounts balance mutation.
const saveAccountsBalanceQueue = async.queue(
this.decreaseInvoicedExpenseQueue,
10
);
const filteredEntries = filterEntriesByRefType(
saleInvoiceDTO.entries,
ProjectLinkRefType.Task
);
filteredEntries.forEach((entry) => {
saveAccountsBalanceQueue.push({
tenantId,
projectRefId: entry.projectRefId,
projectRefInvoicedAmount: entry.projectRefInvoicedAmount,
trx,
});
});
if (filteredEntries.length > 0) {
await saveAccountsBalanceQueue.drain();
}
};
/**
* Queue job increases the invoiced amount of the given bill.
* @param {IncreaseInvoicedTaskQueuePayload} - payload
*/
private increaseInvoicedExpenseQueue = async ({
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx,
}) => {
await this.projectBillableExpense.increaseInvoicedExpense(
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx
);
};
/**
* Queue job decreases the invoiced amount of the given bill.
* @param {IncreaseInvoicedTaskQueuePayload} - payload
*/
private decreaseInvoicedExpenseQueue = async ({
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx,
}) => {
await this.projectBillableExpense.decreaseInvoicedExpense(
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx
);
};
}

View File

@@ -0,0 +1,84 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import { ProjectBillableTask } from './ProjectBillableTasks';
import { ProjectBillableExpense } from './ProjectBillableExpense';
import { ProjectBillableExpenseInvoiced } from './ProjectBillableExpenseInvoiced';
@Service()
export class ProjectBillableBillSubscriber {
@Inject()
private projectBillableExpenseInvoiced: ProjectBillableExpenseInvoiced;
/**
* Attaches events with handlers.
* @param bus
*/
attach(bus) {
bus.subscribe(
events.saleInvoice.onCreated,
this.handleIncreaseBillableBill
);
bus.subscribe(events.saleInvoice.onEdited, this.handleDecreaseBillableBill);
bus.subscribe(events.saleInvoice.onDeleted, this.handleEditBillableBill);
}
/**
* Increases the billable amount of expense.
* @param {ISaleInvoiceCreatedPayload} payload -
*/
public handleIncreaseBillableBill = async ({
tenantId,
saleInvoice,
saleInvoiceDTO,
trx,
}: ISaleInvoiceCreatedPayload) => {
await this.projectBillableExpenseInvoiced.increaseInvoicedExpense(
tenantId,
saleInvoiceDTO,
trx
);
};
/**
* Decreases the billable amount of expense.
* @param {ISaleInvoiceDeletedPayload} payload -
*/
public handleDecreaseBillableBill = async ({
tenantId,
oldSaleInvoice,
trx,
}: ISaleInvoiceDeletedPayload) => {
await this.projectBillableExpenseInvoiced.decreaseInvoicedExpense(
tenantId,
oldSaleInvoice,
trx
);
};
/**
*
* @param {ISaleInvoiceEditedPayload} payload -
*/
public handleEditBillableBill = async ({
tenantId,
oldSaleInvoice,
saleInvoiceDTO,
trx,
}: ISaleInvoiceEditedPayload) => {
await this.projectBillableExpenseInvoiced.decreaseInvoicedExpense(
tenantId,
oldSaleInvoice,
trx
);
await this.projectBillableExpenseInvoiced.increaseInvoicedExpense(
tenantId,
saleInvoiceDTO,
trx
);
};
}

View File

@@ -0,0 +1,101 @@
import { IBill } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class ProjectBillableBillTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'billableType',
'billableId',
'billableAmount',
'billableAmountFormatted',
'billableCurrency',
'billableTransactionNo',
'billableDate',
'billableDateFormatted',
];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Billable type.
* @returns {string}
*/
public billableType = () => {
return 'Bill';
};
/**
* Billable id.
* @param {IBill} bill
* @returns {string}
*/
public billableId = (bill: IBill) => {
return bill.id;
};
/**
* Billable amount.
* @param {IBill} bill
* @returns {string}
*/
public billableAmount = (bill: IBill) => {
return bill.billableAmount;
};
/**
* Billable amount formatted.
* @param {IBill} bill
* @returns {string}
*/
public billableAmountFormatted = (bill: IBill) => {
return formatNumber(bill.billableAmount, {
currencyCode: bill.currencyCode,
});
};
/**
* Billable currency.
* @param {IBill} bill
* @returns {string}
*/
public billableCurrency = (bill: IBill) => {
return bill.currencyCode;
};
/**
*
* @param {IBill} bill
* @returns {string}
*/
public billableTransactionNo = (bill: IBill) => {
return bill.billNumber;
};
/**
* Billable date.
* @returns {Date}
*/
public billableDate = (bill: IBill) => {
return bill.createdAt;
};
/**
* Billable date formatted.
* @returns {string}
*/
public billableDateFormatted = (bill: IBill) => {
return this.formatDate(bill.createdAt);
};
}

View File

@@ -0,0 +1,49 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class ProjectBillableExpense {
@Inject()
private tenancy: HasTenancyService;
/**
* Increase the invoiced amount of the given expense.
* @param {number} tenantId
* @param {number} expenseId
* @param {number} invoicedAmount
* @param {Knex.Transaction} trx
*/
public increaseInvoicedExpense = async (
tenantId: number,
expenseId: number,
invoicedAmount: number,
trx?: Knex.Transaction
) => {
const { Expense } = this.tenancy.models(tenantId);
await Expense.query(trx)
.findById(expenseId)
.increment('invoicedAmount', invoicedAmount);
};
/**
* Decrease the invoiced amount of the given expense.
* @param {number} tenantId
* @param {number} taskId
* @param {number} invoiceHours
* @param {Knex.Transaction} knex
*/
public decreaseInvoicedExpense = async (
tenantId: number,
expenseId: number,
invoiceHours: number,
trx?: Knex.Transaction
) => {
const { Expense } = this.tenancy.models(tenantId);
await Expense.query(trx)
.findById(expenseId)
.decrement('invoicedAmount', invoiceHours);
};
}

View File

@@ -0,0 +1,116 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import async from 'async';
import { ISaleInvoice, ISaleInvoiceDTO, ProjectLinkRefType } from '@/interfaces';
import { ProjectBillableExpense } from './ProjectBillableExpense';
import { filterEntriesByRefType } from './_utils';
@Service()
export class ProjectBillableExpenseInvoiced {
@Inject()
private projectBillableExpense: ProjectBillableExpense;
/**
* Increases the invoiced amount of invoice entries that reference to
* expense entries.
* @param {number} tenantId
* @param {ISaleInvoice | ISaleInvoiceDTO} saleInvoiceDTO
* @param {Knex.Transaction} trx
*/
public increaseInvoicedExpense = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoice | ISaleInvoiceDTO,
trx?: Knex.Transaction
) => {
// Initiates a new queue for accounts balance mutation.
const saveAccountsBalanceQueue = async.queue(
this.increaseInvoicedExpenseQueue,
10
);
const filteredEntries = filterEntriesByRefType(
saleInvoiceDTO.entries,
ProjectLinkRefType.Expense
);
filteredEntries.forEach((entry) => {
saveAccountsBalanceQueue.push({
tenantId,
projectRefId: entry.projectRefId,
projectRefInvoicedAmount: entry.projectRefInvoicedAmount,
trx,
});
});
if (filteredEntries.length > 0) {
await saveAccountsBalanceQueue.drain();
}
};
/**
* Decreases the invoiced amount of the given expenses from
* the invoice entries.
* @param {number} tenantId
* @param {ISaleInvoice | ISaleInvoiceDTO} saleInvoiceDTO
* @param {Knex.Transaction} trx
*/
public decreaseInvoicedExpense = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoice | ISaleInvoiceDTO,
trx?: Knex.Transaction
) => {
// Initiates a new queue for accounts balance mutation.
const saveAccountsBalanceQueue = async.queue(
this.decreaseInvoicedExpenseQueue,
10
);
const filteredEntries = filterEntriesByRefType(
saleInvoiceDTO.entries,
ProjectLinkRefType.Expense
);
filteredEntries.forEach((entry) => {
saveAccountsBalanceQueue.push({
tenantId,
projectRefId: entry.projectRefId,
projectRefInvoicedAmount: entry.projectRefInvoicedAmount,
trx,
});
});
if (filteredEntries.length > 0) {
await saveAccountsBalanceQueue.drain();
}
};
/**
* Queue job increases the invoiced amount of the given expense.
* @param {IncreaseInvoicedTaskQueuePayload} - payload
*/
private increaseInvoicedExpenseQueue = async ({
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx,
}) => {
await this.projectBillableExpense.increaseInvoicedExpense(
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx
);
};
/**
* Queue job decreases the invoiced amount of the given expense.
* @param {IncreaseInvoicedTaskQueuePayload} - payload
*/
private decreaseInvoicedExpenseQueue = async ({
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx,
}) => {
await this.projectBillableExpense.decreaseInvoicedExpense(
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx
);
};
}

View File

@@ -0,0 +1,87 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import { ProjectBillableExpenseInvoiced } from './ProjectBillableExpenseInvoiced';
@Service()
export class ProjectBillableExpensesSubscriber {
@Inject()
private projectBillableExpenseInvoiced: ProjectBillableExpenseInvoiced;
/**
* Attaches events with handlers.
* @param bus
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onCreated,
this.handleIncreaseBillableExpenses
);
bus.subscribe(
events.saleInvoice.onEdited,
this.handleDecreaseBillableExpenses
);
bus.subscribe(
events.saleInvoice.onDeleted,
this.handleEditBillableExpenses
);
}
/**
* Increases the billable amount of expense.
* @param {ISaleInvoiceCreatedPayload} payload -
*/
public handleIncreaseBillableExpenses = async ({
tenantId,
saleInvoiceDTO,
trx,
}: ISaleInvoiceCreatedPayload) => {
await this.projectBillableExpenseInvoiced.increaseInvoicedExpense(
tenantId,
saleInvoiceDTO,
trx
);
};
/**
* Decreases the billable amount of expense.
* @param {ISaleInvoiceDeletedPayload} payload -
*/
public handleDecreaseBillableExpenses = async ({
tenantId,
oldSaleInvoice,
trx,
}: ISaleInvoiceDeletedPayload) => {
await this.projectBillableExpenseInvoiced.increaseInvoicedExpense(
tenantId,
oldSaleInvoice,
trx
);
};
/**
* Decreases the old invoice and increases the new invoice DTO.
* @param {ISaleInvoiceEditedPayload} payload -
*/
public handleEditBillableExpenses = async ({
tenantId,
saleInvoice,
oldSaleInvoice,
trx,
}: ISaleInvoiceEditedPayload) => {
await this.projectBillableExpenseInvoiced.decreaseInvoicedExpense(
tenantId,
oldSaleInvoice,
trx
);
await this.projectBillableExpenseInvoiced.increaseInvoicedExpense(
tenantId,
saleInvoice,
trx
);
};
}

View File

@@ -0,0 +1,100 @@
import { IExpense } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class ProjectBillableExpenseTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'billableType',
'billableId',
'billableAmount',
'billableAmountFormatted',
'billableCurrency',
'billableTransactionNo',
'billableDate',
'billableDateFormatted',
];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Retrieves the billable type.
* @returns {string}
*/
public billableType = () => {
return 'Expense';
};
/**
* Retrieves the billable id.
* @param {IExpense} expense
* @returns {string}
*/
public billableId = (expense: IExpense) => {
return expense.id;
};
/**
* Retrieves the billable amount of expense.
* @param {IExpense} expense -
* @returns {number}
*/
public billableAmount = (expense: IExpense) => {
return expense.billableAmount;
};
/**
* Retrieves the billable formatted amount of expense.
* @param {IExpense} expense
* @returns {string}
*/
public billableAmountFormatted = (expense: IExpense) => {
return formatNumber(expense.billableAmount, {
currencyCode: expense.currencyCode,
});
};
/**
* Retrieves the currency of billable expense.
* @param {IExpense} expense
* @returns {string}
*/
public billableCurrency = (expense: IExpense) => {
return expense.currencyCode;
};
/**
* Billable transaction number.
* @returns {string}
*/
public billableTransactionNo = () => {
return '';
};
/**
* Billable date.
* @returns {Date}
*/
public billableDate = (expense: IExpense) => {
return expense.createdAt;
};
/**
* Billable date formatted.
* @returns {string}
*/
public billableDateFormatted = (expense: IExpense) => {
return this.formatDate(expense.createdAt);
};
}

View File

@@ -0,0 +1,108 @@
import { IProjectTask } from '@/interfaces';
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatNumber } from 'utils';
export class ProjectBillableTaskTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'billableType',
'billableId',
'billableAmount',
'billableAmountFormatted',
'billableHours',
'billableCurrency',
'billableTransactionNo',
'billableDate',
'billableDateFormatted',
];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['*'];
};
/**
* Billable type.
* @returns {string}
*/
public billableType = () => {
return 'Task';
};
/**
* Billable id.
* @param {IProjectTask} task
* @returns {string}
*/
public billableId = (task: IProjectTask) => {
return task.id;
};
/**
* Billable amount.
* @param {IProjectTask} task
* @returns {number}
*/
public billableAmount = (task: IProjectTask) => {
return task.billableAmount;
};
/**
* Billable amount formatted.
* @returns {string}
*/
public billableAmountFormatted = (task: IProjectTask) => {
return formatNumber(task.billableAmount, {
currencyCode: this.context.baseCurrency,
});
};
/**
* Billable hours of the task.
* @param {IProjectTask} task
* @returns {number}
*/
public billableHours = (task: IProjectTask) => {
return task.billableHours;
};
/**
* Retrieves the currency of billable entry.
* @returns {string}
*/
public billableCurrency = () => {
return this.context.baseCurrency;
};
/**
* Billable transaction number.
* @returns {string}
*/
public billableTransactionNo = () => {
return '';
};
/**
* Billable date.
* @returns {Date}
*/
public billableDate = (task: IProjectTask) => {
return task.createdAt;
};
/**
* Billable date formatted.
* @returns {string}
*/
public billableDateFormatted = (task: IProjectTask) => {
return this.formatDate(task.createdAt);
};
}

View File

@@ -0,0 +1,48 @@
import { Service, Inject } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Knex } from 'knex';
@Service()
export class ProjectBillableTask {
@Inject()
private tenancy: HasTenancyService;
/**
* Increase the invoiced hours of the given task.
* @param {number} tenantId
* @param {number} taskId
* @param {number} invoiceHours
*/
public increaseInvoicedTask = async (
tenantId: number,
taskId: number,
invoiceHours: number,
trx?: Knex.Transaction
) => {
const { Task } = this.tenancy.models(tenantId);
await Task.query(trx)
.findById(taskId)
.increment('invoicedHours', invoiceHours);
};
/**
* Decrease the invoiced hours of the given task.
* @param {number} tenantId
* @param {number} taskId
* @param {number} invoiceHours -
* @returns {}
*/
public decreaseInvoicedTask = async (
tenantId: number,
taskId: number,
invoiceHours: number,
trx?: Knex.Transaction
) => {
const { Task } = this.tenancy.models(tenantId);
await Task.query(trx)
.findById(taskId)
.decrement('invoicedHours', invoiceHours);
};
}

View File

@@ -0,0 +1,116 @@
import async from 'async';
import { Inject, Service } from 'typedi';
import { Knex } from 'knex';
import { ISaleInvoice, ISaleInvoiceDTO, ProjectLinkRefType } from '@/interfaces';
import { ProjectBillableTask } from './ProjectBillableTasks';
import { filterEntriesByRefType } from './_utils';
import { IncreaseInvoicedTaskQueuePayload } from './_types';
@Service()
export class ProjectBillableTasksInvoiced {
@Inject()
private projectBillableTasks: ProjectBillableTask;
/**
* Increases the invoiced amount of the given tasks that associated
* to the invoice entries.
* @param {number} tenantId
* @param {ISaleInvoiceDTO} saleInvoiceDTO
*/
public increaseInvoicedTasks = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoiceDTO | ISaleInvoice,
trx?: Knex.Transaction
) => {
// Initiate a new queue for accounts balance mutation.
const saveAccountsBalanceQueue = async.queue(
this.increaseInvoicedTaskQueue,
10
);
const filteredEntries = filterEntriesByRefType(
saleInvoiceDTO.entries,
ProjectLinkRefType.Task
);
filteredEntries.forEach((entry) => {
saveAccountsBalanceQueue.push({
tenantId,
projectRefId: entry.projectRefId,
projectRefInvoicedAmount: entry.projectRefInvoicedAmount,
trx,
});
});
if (filteredEntries.length > 0) {
await saveAccountsBalanceQueue.drain();
}
};
/**
* Decreases the invoiced amount of the given tasks that associated
* to the invoice entries.
* @param {number} tenantId
* @param {ISaleInvoiceDTO | ISaleInvoice} saleInvoiceDTO
* @param {Knex.Transaction} trx
*/
public decreaseInvoicedTasks = async (
tenantId: number,
saleInvoiceDTO: ISaleInvoiceDTO | ISaleInvoice,
trx?: Knex.Transaction
) => {
// Initiate a new queue for accounts balance mutation.
const saveAccountsBalanceQueue = async.queue(
this.decreaseInvoicedTaskQueue,
10
);
const filteredEntries = filterEntriesByRefType(
saleInvoiceDTO.entries,
ProjectLinkRefType.Task
);
filteredEntries.forEach((entry) => {
saveAccountsBalanceQueue.push({
tenantId,
projectRefId: entry.projectRefId,
projectRefInvoicedAmount: entry.projectRefInvoicedAmount,
trx,
});
});
if (filteredEntries.length > 0) {
await saveAccountsBalanceQueue.drain();
}
};
/**
* Queue job increases the invoiced amount of the given task.
* @param {IncreaseInvoicedTaskQueuePayload} - payload
*/
private increaseInvoicedTaskQueue = async ({
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx,
}: IncreaseInvoicedTaskQueuePayload) => {
await this.projectBillableTasks.increaseInvoicedTask(
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx
);
};
/**
* Queue jobs decreases the invoiced amount of the given task.
* @param {IncreaseInvoicedTaskQueuePayload} - payload
*/
private decreaseInvoicedTaskQueue = async ({
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx,
}) => {
await this.projectBillableTasks.decreaseInvoicedTask(
tenantId,
projectRefId,
projectRefInvoicedAmount,
trx
);
};
}

View File

@@ -0,0 +1,104 @@
import { Inject, Service } from 'typedi';
import events from '@/subscribers/events';
import {
ISaleInvoiceCreatedPayload,
ISaleInvoiceDeletedPayload,
ISaleInvoiceEditedPayload,
} from '@/interfaces';
import { ProjectInvoiceValidator } from './ProjectInvoiceValidator';
import { ProjectBillableTasksInvoiced } from './ProjectBillableTasksInvoiced';
@Service()
export class ProjectBillableTasksSubscriber {
@Inject()
private projectBillableTasks: ProjectBillableTasksInvoiced;
@Inject()
private projectBillableTasksValidator: ProjectInvoiceValidator;
/**
* Attaches events with handlers.
* @param bus
*/
public attach(bus) {
bus.subscribe(
events.saleInvoice.onCreating,
this.handleValidateInvoiceTasksRefs
);
bus.subscribe(
events.saleInvoice.onCreated,
this.handleIncreaseBillableTasks
);
bus.subscribe(events.saleInvoice.onEdited, this.handleEditBillableTasks);
bus.subscribe(
events.saleInvoice.onDeleted,
this.handleDecreaseBillableTasks
);
}
/**
* Validate the tasks refs ids existance.
* @param {ISaleInvoiceCreatedPayload} payload -
*/
public handleValidateInvoiceTasksRefs = async ({
tenantId,
saleInvoiceDTO,
}: ISaleInvoiceCreatedPayload) => {
await this.projectBillableTasksValidator.validateTasksRefsExistance(
tenantId,
saleInvoiceDTO
);
};
/**
* Handle increase the invoiced tasks once the sale invoice be created.
* @param {ISaleInvoiceCreatedPayload} payload -
*/
public handleIncreaseBillableTasks = async ({
tenantId,
saleInvoiceDTO,
}: ISaleInvoiceCreatedPayload) => {
await this.projectBillableTasks.increaseInvoicedTasks(
tenantId,
saleInvoiceDTO
);
};
/**
* Handle decrease the invoiced tasks once the sale invoice be deleted.
* @param {ISaleInvoiceDeletedPayload} payload -
*/
public handleDecreaseBillableTasks = async ({
tenantId,
oldSaleInvoice,
trx,
}: ISaleInvoiceDeletedPayload) => {
await this.projectBillableTasks.decreaseInvoicedTasks(
tenantId,
oldSaleInvoice,
trx
);
};
/**
* Handle adjusting the invoiced tasks once the sale invoice be edited.
* @param {ISaleInvoiceEditedPayload} payload -
*/
public handleEditBillableTasks = async ({
tenantId,
oldSaleInvoice,
saleInvoiceDTO,
trx,
}: ISaleInvoiceEditedPayload) => {
await this.projectBillableTasks.increaseInvoicedTasks(
tenantId,
saleInvoiceDTO,
trx
);
await this.projectBillableTasks.decreaseInvoicedTasks(
tenantId,
oldSaleInvoice,
trx
);
};
}

View File

@@ -0,0 +1,391 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { sumBy } from 'lodash';
import Project from 'models/Project';
import { formatNumber } from 'utils';
import { formatMinutes } from 'utils/formatMinutes';
export class ProjectDetailedTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'costEstimateFormatted',
'deadlineFormatted',
'contactDisplayName',
'statusFormatted',
'totalActualHours',
'totalActualHoursFormatted',
'totalEstimateHours',
'totalEstimateHoursFormatted',
'totalInvoicedHours',
'totalInvoicedHoursFormatted',
'totalBillableHours',
'totalBillableHoursFormatted',
'totalActualHoursAmount',
'totalActualHoursAmountFormatted',
'totalEstimateHoursAmount',
'totalEstimateHoursAmountFormatted',
'totalInvoicedHoursAmount',
'totalInvoicedHoursAmountFormatted',
'totalBillableHoursAmount',
'totalBillableHoursAmountFormatted',
'totalExpenses',
'totalExpensesFormatted',
'totalInvoicedExpenses',
'totalInvoicedExpensesFormatted',
'totalBillableExpenses',
'totalBillableExpensesFormatted',
'totalInvoiced',
'totalInvoicedFormatted',
'totalBillable',
'totalBillableFormatted',
];
};
/**
* Exclude these attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['contact', 'tasks', 'expenses', 'bills'];
};
/**
* Retrieves the formatted value of cost estimate.
* @param {Project} project
* @returns {string}
*/
public costEstimateFormatted = (project: Project) => {
return formatNumber(project.costEstimate, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieves the formatted value of the deadline date.
* @param {Project} project
* @returns {string}
*/
public deadlineFormatted = (project: Project) => {
return this.formatDate(project.deadline);
};
/**
* Retrieves the contact display name.
* @param {Project} project
* @returns {string}
*/
public contactDisplayName = (project: Project) => {
return project.contact.displayName;
};
/**
* Retrieves the formatted value of project's status.
* @param {Project} project
* @returns {string}
*/
public statusFormatted = (project: Project) => {
return project.status;
};
// --------------------------------------------------------------
// # Tasks Hours
// --------------------------------------------------------------
/**
* Total actual hours.
* @param {Project} project
* @returns {number}
*/
public totalActualHours = (project: Project) => {
return sumBy(project.tasks, 'totalActualHours');
};
/**
* Retrieves the formatted total actual hours.
* @param {Project} project
* @returns {string}
*/
public totalActualHoursFormatted = (project: Project) => {
const hours = this.totalActualHours(project);
return formatMinutes(hours);
};
/**
* Total Estimated hours.
* @param {Project} project
* @returns {number}
*/
public totalEstimateHours = (project: Project) => {
return sumBy(project.tasks, 'totalEstimateHours');
};
/**
* Total estimate hours formatted.
* @param {Project} project
* @returns {string}
*/
public totalEstimateHoursFormatted = (project: Project) => {
const hours = this.totalEstimateHours(project);
return formatMinutes(hours);
};
/**
* Total invoiced hours.
* @param {Project} project
* @returns {number}
*/
public totalInvoicedHours = (project: Project) => {
return sumBy(project.tasks, 'totalInvoicedHours');
};
/**
* Total invoiced hours formatted.
* @param {Project} project
* @returns {string}
*/
public totalInvoicedHoursFormatted = (project: Project) => {
const hours = this.totalInvoicedHours(project);
return formatMinutes(hours);
};
/**
* Total billable hours.
* @param {Project} project
* @returns {number}
*/
public totalBillableHours = (project: Project) => {
const totalActualHours = this.totalActualHours(project);
const totalInvoicedHours = this.totalInvoicedHours(project);
return Math.max(totalActualHours - totalInvoicedHours, 0);
};
/**
* Retrieves the billable hours formatted.
* @param {Project} project
* @returns {string}
*/
public totalBillableHoursFormatted = (project) => {
const hours = this.totalBillableHours(project);
return formatMinutes(hours);
};
// --------------------------------------------------------------
// # Tasks Hours Amount
// --------------------------------------------------------------
/**
* Total amount of invoiced hours.
* @param {Project} project
* @returns {number}
*/
public totalActualHoursAmount = (project: Project) => {
return sumBy(project.tasks, 'totalActualAmount');
};
/**
* Total amount of invoiced hours.
* @param {Project} project
* @returns {number}
*/
public totalActualHoursAmountFormatted = (project: Project) => {
return formatNumber(this.totalActualHoursAmount(project), {
currencyCode: this.context.baseCurrency,
});
};
/**
* Total amount of estimated hours.
* @param {Project} project
* @returns {number}
*/
public totalEstimateHoursAmount = (project: Project) => {
return sumBy(project.tasks, 'totalEstimateAmount');
};
/**
* Formatted amount of total estimate hours.
* @param {Project} project
* @returns {string}
*/
public totalEstimateHoursAmountFormatted = (project: Project) => {
return formatNumber(this.totalEstimateHoursAmount(project), {
currencyCode: this.context.baseCurrency,
});
};
/**
* Total amount of invoiced hours.
* @param {Project} project
* @returns {number}
*/
public totalInvoicedHoursAmount = (project) => {
return sumBy(project.tasks, 'totalInvoicedAmount');
};
/**
* Formatted total amount of invoiced hours.
* @param {Project} project
* @returns {number}
*/
public totalInvoicedHoursAmountFormatted = (project) => {
return formatNumber(this.totalInvoicedHoursAmount(project), {
currencyCode: this.context.baseCurrency,
});
};
/**
* Total amount of billable hours.
* @param {Project} project
* @returns {number}
*/
public totalBillableHoursAmount = (project) => {
const totalActualAmount = this.totalActualHoursAmount(project);
const totalBillableAmount = this.totalInvoicedHoursAmount(project);
return Math.max(totalActualAmount, totalBillableAmount);
};
/**
* Formatted total amount of billable hours.
* @param {Project} project
* @returns {string}
*/
public totalBillableHoursAmountFormatted = (project) => {
return formatNumber(this.totalBillableHoursAmount(project), {
currencyCode: this.context.baseCurrency,
});
};
// --------------------------------------------------------------
// # Expenses
// --------------------------------------------------------------
/**
* Total expenses amount.
* @param {Project} project
* @returns {number}
*/
public totalExpenses = (project) => {
const expensesTotal = sumBy(project.expenses, 'totalExpenses');
const billsTotal = sumBy(project.bills, 'totalBills');
return expensesTotal + billsTotal;
};
/**
* Formatted total amount of expenses.
* @param {Project} project
* @returns {string}
*/
public totalExpensesFormatted = (project) => {
return formatNumber(this.totalExpenses(project), {
currencyCode: this.context.baseCurrency,
});
};
/**
* Total amount of invoiced expenses.
* @param {Project} project
* @returns {number}
*/
public totalInvoicedExpenses = (project: Project) => {
const totalInvoicedExpenses = sumBy(
project.expenses,
'totalInvoicedExpenses'
);
const totalInvoicedBills = sumBy(project.bills, 'totalInvoicedBills');
return totalInvoicedExpenses + totalInvoicedBills;
};
/**
* Formatted total amount of invoiced expenses.
* @param {Project} project
* @returns {string}
*/
public totalInvoicedExpensesFormatted = (project: Project) => {
return formatNumber(this.totalInvoicedExpenses(project), {
currencyCode: this.context.baseCurrency,
});
};
/**
* Total amount of billable expenses.
* @param {Project} project
* @returns {number}
*/
public totalBillableExpenses = (project: Project) => {
const totalInvoiced = this.totalInvoicedExpenses(project);
const totalInvoice = this.totalExpenses(project);
return totalInvoice - totalInvoiced;
};
/**
* Formatted total amount of billable expenses.
* @param {Project} project
* @returns {string}
*/
public totalBillableExpensesFormatted = (project: Project) => {
return formatNumber(this.totalBillableExpenses(project), {
currencyCode: this.context.baseCurrency,
});
};
// --------------------------------------------------------------
// # Total
// --------------------------------------------------------------
/**
* Total invoiced amount.
* @param {Project} project
* @returns {number}
*/
public totalInvoiced = (project: Project) => {
const invoicedExpenses = this.totalInvoicedExpenses(project);
const invoicedTasks = this.totalInvoicedHoursAmount(project);
return invoicedExpenses + invoicedTasks;
};
/**
* Formatted amount of total invoiced.
* @param {Project} project
* @returns {number}
*/
public totalInvoicedFormatted = (project: Project) => {
return formatNumber(this.totalInvoiced(project), {
currencyCode: this.context.baseCurrency,
});
};
/**
* Total billable amount.
* @param {Project} project
* @returns {number}
*/
public totalBillable = (project: Project) => {
const billableExpenses = this.totalBillableExpenses(project);
const billableTasks = this.totalBillableHoursAmount(project);
return billableExpenses + billableTasks;
};
/**
* Formatted amount of billable total.
* @param {Project} project
* @returns {string}
*/
public totalBillableFormatted = (project: Project) => {
return formatNumber(this.totalBillable(project), {
currencyCode: this.context.baseCurrency,
});
};
}

View File

@@ -0,0 +1,48 @@
import { ServiceError } from '@/exceptions';
import { ISaleInvoiceCreateDTO, ProjectLinkRefType } from '@/interfaces';
import { difference, isEmpty } from 'lodash';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { ERRORS } from './constants';
@Service()
export class ProjectInvoiceValidator {
@Inject()
tenancy: HasTenancyService;
/**
* Validate the tasks refs ids existance.
* @param {number} tenantId
* @param {ISaleInvoiceCreateDTO} saleInvoiceDTO
* @returns {Promise<void>}
*/
async validateTasksRefsExistance(
tenantId: number,
saleInvoiceDTO: ISaleInvoiceCreateDTO
) {
const { Task } = this.tenancy.models(tenantId);
// Filters the invoice entries that have `Task` type and not empty ref. id.
const tasksRefs = saleInvoiceDTO.entries.filter(
(entry) =>
entry?.projectRefType === ProjectLinkRefType.Task &&
!isEmpty(entry?.projectRefId)
);
//
if (!tasksRefs.length || (tasksRefs.length && !saleInvoiceDTO.projectId)) {
return;
}
const tasksRefsIds = tasksRefs.map((ref) => ref.projectRefId);
const tasks = await Task.query()
.whereIn('id', tasksRefsIds)
.where('projectId', saleInvoiceDTO.projectId);
const tasksIds = tasks.map((task) => task.id);
const notFoundTasksIds = difference(tasksIds, tasksRefsIds);
if (!notFoundTasksIds.length) {
throw new ServiceError(ERRORS.ITEM_ENTRIES_REF_IDS_NOT_FOUND);
}
}
}

View File

@@ -0,0 +1,64 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import Project from 'models/Project';
import { formatNumber } from 'utils';
export class ProjectTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'costEstimateFormatted',
'deadlineFormatted',
'contactDisplayName',
'statusFormatted',
];
};
/**
* Exclude these attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['contact'];
};
/**
* Retrieves the formatted value of cost estimate.
* @param {Project} project
* @returns {string}
*/
public costEstimateFormatted = (project: Project) => {
return formatNumber(project.costEstimate, {
currencyCode: this.context.organization.baseCurrency,
});
};
/**
* Retrieves the formatted value of the deadline date.
* @param {Project} project
* @returns {string}
*/
public deadlineFormatted = (project: Project) => {
return this.formatDate(project.deadline);
};
/**
* Retrieves the contact display name.
* @param {Project} project
* @returns {string}
*/
public contactDisplayName = (project: Project) => {
return project.contact.displayName;
};
/**
* Retrieves the formatted value of project's status.
* @param {Project} project
* @returns {string}
*/
public statusFormatted = (project: Project) => {
return project.status;
};
}

View File

@@ -0,0 +1,147 @@
import { Inject, Service } from 'typedi';
import {
IProjectCreateDTO,
IProjectCreatePOJO,
IProjectEditPOJO,
IProjectGetPOJO,
IProjectStatus,
IVendorsFilter,
ProjectBillableEntriesQuery,
ProjectBillableEntry,
} from '@/interfaces';
import CreateProject from './CreateProject';
import DeleteProject from './DeleteProject';
import GetProject from './GetProject';
import EditProjectService from './EditProject';
import GetProjects from './GetProjects';
import EditProjectStatusService from './EditProjectStatus';
import GetProjectBillableEntries from './GetProjectBillableEntries';
@Service()
export class ProjectsApplication {
@Inject()
private createProjectService: CreateProject;
@Inject()
private editProjectService: EditProjectService;
@Inject()
private deleteProjectService: DeleteProject;
@Inject()
private getProjectService: GetProject;
@Inject()
private getProjectsService: GetProjects;
@Inject()
private editProjectStatusService: EditProjectStatusService;
@Inject()
private getProjectBillable: GetProjectBillableEntries;
/**
* Creates a new project.
* @param {number} tenantId - Tenant id.
* @param {IProjectCreateDTO} projectDTO - Create project DTO.
* @return {Promise<IProjectCreatePOJO>}
*/
public createProject = (
tenantId: number,
projectDTO: IProjectCreateDTO
): Promise<IProjectCreatePOJO> => {
return this.createProjectService.createProject(tenantId, projectDTO);
};
/**
* Edits details of the given vendor.
* @param {number} tenantId - Tenant id.
* @param {number} vendorId - Vendor id.
* @param {IProjectCreateDTO} projectDTO - Create project DTO.
* @returns {Promise<IVendor>}
*/
public editProject = (
tenantId: number,
projectId: number,
projectDTO: IProjectCreateDTO
): Promise<IProjectEditPOJO> => {
return this.editProjectService.editProject(tenantId, projectId, projectDTO);
};
/**
* Deletes the given project.
* @param {number} tenantId
* @param {number} vendorId
* @return {Promise<void>}
*/
public deleteProject = (
tenantId: number,
projectId: number
): Promise<void> => {
return this.deleteProjectService.deleteProject(tenantId, projectId);
};
/**
* Retrieves the vendor details.
* @param {number} tenantId
* @param {number} projectId
* @returns {Promise<IProjectGetPOJO>}
*/
public getProject = (
tenantId: number,
projectId: number
): Promise<IProjectGetPOJO> => {
return this.getProjectService.getProject(tenantId, projectId);
};
/**
* Retrieves the vendors paginated list.
* @param {number} tenantId
* @param {IVendorsFilter} filterDTO
* @returns {Promise<IProjectGetPOJO[]>}
*/
public getProjects = (
tenantId: number,
filterDTO: IVendorsFilter
): Promise<IProjectGetPOJO[]> => {
return this.getProjectsService.getProjects(tenantId);
};
/**
* Edits the given project status.
* @param {number} tenantId
* @param {number} projectId
* @param {IProjectStatus} status
* @returns {Promise<IProject>}
*/
public editProjectStatus = (
tenantId: number,
projectId: number,
status: IProjectStatus
) => {
return this.editProjectStatusService.editProjectStatus(
tenantId,
projectId,
status
);
};
/**
* Retrieves the billable entries of the given project.
* @param {number} tenantId
* @param {number} projectId
* @param {ProjectBillableEntriesQuery} query
* @returns {Promise<ProjectBillableEntry[]>}
*/
public getProjectBillableEntries = (
tenantId: number,
projectId: number,
query?: ProjectBillableEntriesQuery
): Promise<ProjectBillableEntry[]> => {
return this.getProjectBillable.getProjectBillableEntries(
tenantId,
projectId,
query
);
};
}

View File

@@ -0,0 +1,23 @@
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
@Service()
export class ProjectsValidator {
@Inject()
private tenancy: HasTenancyService;
/**
* Validate contact exists.
* @param {number} tenantId
* @param {number} contactId
*/
public async validateContactExists(tenantId: number, contactId: number) {
const { Contact } = this.tenancy.models(tenantId);
// Validate customer existance.
await Contact.query()
.modify('customer')
.findById(contactId)
.throwIfNotFound();
}
}

View File

@@ -0,0 +1,22 @@
import {
ProjectBillableEntriesQuery,
ProjectBillableEntry,
ProjectBillableType,
} from '@/interfaces';
import { Knex } from 'knex';
export interface IncreaseInvoicedTaskQueuePayload {
tenantId: number;
projectRefId: number;
projectRefInvoicedAmount: number;
trx?: Knex.Transaction;
}
export interface ProjectBillableGetter {
type: ProjectBillableType;
getter: (
tenantId: number,
projectId: number,
query: ProjectBillableEntriesQuery
) => Promise<ProjectBillableEntry[]>;
}

View File

@@ -0,0 +1,8 @@
import { IItemEntry, IItemEntryDTO } from '@/interfaces';
export const filterEntriesByRefType = (
entries: (IItemEntry | IItemEntryDTO)[],
projectRefType: string
) => {
return entries.filter((entry) => entry.projectRefType === projectRefType);
};

View File

@@ -0,0 +1,3 @@
export enum ERRORS {
ITEM_ENTRIES_REF_IDS_NOT_FOUND = 'ITEM_ENTRIES_REF_IDS_NOT_FOUND',
}

View File

@@ -0,0 +1,74 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
ICreateTaskDTO,
IProjectTaskCreatePOJO,
ITaskCreatedEventPayload,
ITaskCreateEventPayload,
ITaskCreatingEventPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class CreateTaskService {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates a new task.
* @param {number} tenantId -
* @param {number} projectId - Project id.
* @param {ICreateTaskDTO} taskDTO - Project's task DTO.
* @returns {Promise<IProjectTaskCreatePOJO>}
*/
public createTask = async (
tenantId: number,
projectId: number,
taskDTO: ICreateTaskDTO
): Promise<IProjectTaskCreatePOJO> => {
const { Task, Project } = this.tenancy.models(tenantId);
// Validate project existance.
const project = await Project.query().findById(projectId).throwIfNotFound();
// Triggers `onProjectTaskCreate` event.
await this.eventPublisher.emitAsync(events.projectTask.onCreate, {
tenantId,
taskDTO,
} as ITaskCreateEventPayload);
// Creates a new project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onProjectTaskCreating` event.
await this.eventPublisher.emitAsync(events.projectTask.onCreating, {
tenantId,
taskDTO,
trx,
} as ITaskCreatingEventPayload);
const task = await Task.query().insert({
...taskDTO,
actualHours: 0,
projectId,
});
// Triggers `onProjectTaskCreated` event.
await this.eventPublisher.emitAsync(events.projectTask.onCreated, {
tenantId,
taskDTO,
task,
trx,
} as ITaskCreatedEventPayload);
return task;
});
};
}

View File

@@ -0,0 +1,64 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
ITaskDeletedEventPayload,
ITaskDeleteEventPayload,
ITaskDeletingEventPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class DeleteTaskService {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Deletes the give project.
* @param {number} projectId -
* @returns {Promise<void>}
*/
public deleteTask = async (
tenantId: number,
taskId: number
): Promise<void> => {
const { Task } = this.tenancy.models(tenantId);
// Validate customer existance.
const oldTask = await Task.query().findById(taskId).throwIfNotFound();
// Triggers `onDeleteProjectTask` event.
await this.eventPublisher.emitAsync(events.projectTask.onDelete, {
tenantId,
taskId,
} as ITaskDeleteEventPayload);
// Deletes the given project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onProjectDeleting` event.
await this.eventPublisher.emitAsync(events.projectTask.onDeleting, {
tenantId,
oldTask,
trx,
} as ITaskDeletingEventPayload);
// Deletes the project object from the storage.
await Task.query(trx).findById(taskId).delete();
// Triggers `onProjectDeleted` event.
await this.eventPublisher.emitAsync(events.projectTask.onDeleted, {
tenantId,
oldTask,
trx,
} as ITaskDeletedEventPayload);
});
};
}

View File

@@ -0,0 +1,77 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
IEditTaskDTO,
IProjectTaskEditPOJO,
ITaskEditedEventPayload,
ITaskEditEventPayload,
ITaskEditingEventPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class EditTaskService {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Edits a new credit note.
* @param {number} tenantId -
* @param {number} taskId -
* @param {IEditTaskDTO} taskDTO -
* @returns {IProjectTaskEditPOJO}
*/
public editTask = async (
tenantId: number,
taskId: number,
taskDTO: IEditTaskDTO
): Promise<IProjectTaskEditPOJO> => {
const { Task } = this.tenancy.models(tenantId);
// Validate task existance.
const oldTask = await Task.query().findById(taskId).throwIfNotFound();
// Triggers `onProjectTaskEdit` event.
await this.eventPublisher.emitAsync(events.projectTask.onEdit, {
tenantId,
taskId,
taskDTO,
} as ITaskEditEventPayload);
// Edits the given project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onProjectTaskEditing` event.
await this.eventPublisher.emitAsync(events.projectTask.onEditing, {
tenantId,
oldTask,
taskDTO,
trx,
} as ITaskEditingEventPayload);
// Upsert the project's task object.
const task = await Task.query(trx).upsertGraph({
id: taskId,
...taskDTO,
});
// Triggers `onProjectTaskEdited` event.
await this.eventPublisher.emitAsync(events.projectTask.onEdited, {
tenantId,
oldTask,
taskDTO,
task,
trx,
} as ITaskEditedEventPayload);
return task;
});
};
}

View File

@@ -0,0 +1,33 @@
import { IProjectTaskGetPOJO } from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { TaskTransformer } from './TaskTransformer';
@Service()
export class GetTaskService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the tasks list.
* @param {number} tenantId - Tenant Id.
* @param {number} taskId - Task Id.
* @returns {Promise<IProjectTaskGetPOJO>}
*/
public getTask = async (
tenantId: number,
taskId: number
): Promise<IProjectTaskGetPOJO> => {
const { Task } = this.tenancy.models(tenantId);
// Retrieve the project.
const task = await Task.query().findById(taskId).throwIfNotFound();
// Transformes and returns object.
return this.transformer.transform(tenantId, task, new TaskTransformer());
};
}

View File

@@ -0,0 +1,33 @@
import { IProjectTaskGetPOJO } from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { TaskTransformer } from './TaskTransformer';
@Service()
export class GetTasksService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the tasks list.
* @param {number} tenantId - Tenant Id.
* @param {number} taskId - Task Id.
* @returns {}
*/
public getTasks = async (
tenantId: number,
projectId: number
): Promise<IProjectTaskGetPOJO[]> => {
const { Task } = this.tenancy.models(tenantId);
// Retrieve the project.
const tasks = await Task.query().where('projectId', projectId);
// Transformes and returns object.
return this.transformer.transform(tenantId, tasks, new TaskTransformer());
};
}

View File

@@ -0,0 +1,49 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import { formatMinutes } from 'utils/formatMinutes';
export class TaskTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return [
'estimateHoursFormatted',
'actualHoursFormatted',
'invoicedHoursFormatted',
'billableHoursFormatted',
];
};
/**
* Retrieves the formatted estimate hours.
* @returns {string}
*/
public estimateHoursFormatted = (task): string => {
return formatMinutes(task.estimateHours);
};
/**
* Retrieves the formatted actual hours.
* @returns {string}
*/
public actualHoursFormatted = (task): string => {
return formatMinutes(task.actualHours);
};
/**
* Retrieves the formatted billable hours.
* @returns {string}
*/
public billableHoursFormatted = (task): string => {
return formatMinutes(task.billableHours);
};
/**
* Retreives the formatted invoiced hours.
* @returns {string}
*/
public invoicedHoursFormatted = (task): string => {
return formatMinutes(task.invoicedHours);
};
}

View File

@@ -0,0 +1,97 @@
import { Inject, Service } from 'typedi';
import {
ICreateTaskDTO,
IEditTaskDTO,
IProjectTaskCreatePOJO,
IProjectTaskEditPOJO,
IProjectTaskGetPOJO,
} from '@/interfaces';
import { CreateTaskService } from './CreateTask';
import { DeleteTaskService } from './DeleteTask';
import { GetTaskService } from './GetTask';
import { EditTaskService } from './EditTask';
import { GetTasksService } from './GetTasks';
@Service()
export class TasksApplication {
@Inject()
private createTaskService: CreateTaskService;
@Inject()
private editTaskService: EditTaskService;
@Inject()
private deleteTaskService: DeleteTaskService;
@Inject()
private getTaskService: GetTaskService;
@Inject()
private getTasksService: GetTasksService;
/**
* Creates a new task associated to specific project.
* @param {number} tenantId - Tenant id.
* @param {number} project - Project id.
* @param {ICreateTaskDTO} taskDTO - Create project DTO.
* @return {Promise<IProjectTaskCreatePOJO>}
*/
public createTask = (
tenantId: number,
projectId: number,
taskDTO: ICreateTaskDTO
): Promise<IProjectTaskCreatePOJO> => {
return this.createTaskService.createTask(tenantId, projectId, taskDTO);
};
/**
* Edits details of the given task.
* @param {number} tenantId - Tenant id.
* @param {number} vendorId - Vendor id.
* @param {IEditTaskDTO} projectDTO - Create project DTO.
* @returns {Promise<IProjectTaskEditPOJO>}
*/
public editTask = (
tenantId: number,
taskId: number,
taskDTO: IEditTaskDTO
): Promise<IProjectTaskEditPOJO> => {
return this.editTaskService.editTask(tenantId, taskId, taskDTO);
};
/**
* Deletes the given task.
* @param {number} tenantId
* @param {number} taskId - Task id.
* @return {Promise<void>}
*/
public deleteTask = (tenantId: number, taskId: number): Promise<void> => {
return this.deleteTaskService.deleteTask(tenantId, taskId);
};
/**
* Retrieves the given task details.
* @param {number} tenantId
* @param {number} taskId
* @returns {Promise<IProjectTaskGetPOJO>}
*/
public getTask = (
tenantId: number,
taskId: number
): Promise<IProjectTaskGetPOJO> => {
return this.getTaskService.getTask(tenantId, taskId);
};
/**
* Retrieves the vendors paginated list.
* @param {number} tenantId
* @param {IVendorsFilter} filterDTO
* @returns {Promise<IProjectTaskGetPOJO[]>}
*/
public getTasks = (
tenantId: number,
projectId: number
): Promise<IProjectTaskGetPOJO[]> => {
return this.getTasksService.getTasks(tenantId, projectId);
};
}

View File

@@ -0,0 +1,5 @@
export enum ProjectTaskChargeType {
Fixed = 'FIXED',
Time = 'TIME',
NonChargable = 'NON_CHARGABLE',
}

View File

@@ -0,0 +1,72 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
IProjectTimeCreatedEventPayload,
IProjectTimeCreateDTO,
IProjectTimeCreateEventPayload,
IProjectTimeCreatePOJO,
IProjectTimeCreatingEventPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class CreateTimeService {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Creates a new time.
* @param {number} taskId -
* @param {IProjectTimeCreateDTO} timeDTO -
* @returns {Promise<IProjectTimeCreatePOJO>}
*/
public createTime = async (
tenantId: number,
taskId: number,
timeDTO: IProjectTimeCreateDTO
): Promise<IProjectTimeCreatePOJO> => {
const { Time, Task } = this.tenancy.models(tenantId);
const task = await Task.query().findById(taskId).throwIfNotFound();
// Triggers `onProjectTimeCreate` event.
await this.eventPublisher.emitAsync(events.projectTime.onCreate, {
tenantId,
timeDTO,
} as IProjectTimeCreateEventPayload);
// Creates a new project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onProjectTimeCreating` event.
await this.eventPublisher.emitAsync(events.projectTime.onCreating, {
tenantId,
timeDTO,
trx,
} as IProjectTimeCreatingEventPayload);
const time = await Time.query().insert({
...timeDTO,
taskId,
projectId: task.projectId,
});
// Triggers `onProjectTimeCreated` event.
await this.eventPublisher.emitAsync(events.projectTime.onCreated, {
tenantId,
time,
trx,
} as IProjectTimeCreatedEventPayload);
return time;
});
};
}

View File

@@ -0,0 +1,61 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
IProjectTimeDeletedEventPayload,
IProjectTimeDeleteEventPayload,
IProjectTimeDeletingEventPayload,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class DeleteTimeService {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Deletes the give task's time that associated to the given project.
* @param {number} projectId -
* @returns {Promise<void>}
*/
public deleteTime = async (tenantId: number, timeId: number) => {
const { Time } = this.tenancy.models(tenantId);
// Validate customer existance.
const oldTime = await Time.query().findById(timeId).throwIfNotFound();
// Triggers `onProjectDelete` event.
await this.eventPublisher.emitAsync(events.projectTime.onDelete, {
tenantId,
timeId,
} as IProjectTimeDeleteEventPayload);
// Deletes the given project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onProjectDeleting` event.
await this.eventPublisher.emitAsync(events.projectTime.onDeleting, {
tenantId,
oldTime,
trx,
} as IProjectTimeDeletingEventPayload);
// Upsert the project object.
await Time.query(trx).findById(timeId).delete();
// Triggers `onProjectDeleted` event.
await this.eventPublisher.emitAsync(events.projectTime.onDeleted, {
tenantId,
oldTime,
trx,
} as IProjectTimeDeletedEventPayload);
});
};
}

View File

@@ -0,0 +1,76 @@
import { Knex } from 'knex';
import { Service, Inject } from 'typedi';
import {
IProjectTimeEditDTO,
IProjectTimeEditedEventPayload,
IProjectTimeEditEventPayload,
IProjectTimeEditingEventPayload,
IProjectTimeEditPOJO,
} from '@/interfaces';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import UnitOfWork from '@/services/UnitOfWork';
import { EventPublisher } from '@/lib/EventPublisher/EventPublisher';
import events from '@/subscribers/events';
@Service()
export class EditTimeService {
@Inject()
private uow: UnitOfWork;
@Inject()
private tenancy: HasTenancyService;
@Inject()
private eventPublisher: EventPublisher;
/**
* Edits the given project's time that associated to the given task.
* @param {number} tenantId - Tenant id.
* @param {number} taskId - Task id.
* @returns {Promise<IProjectTimeEditPOJO>}
*/
public editTime = async (
tenantId: number,
timeId: number,
timeDTO: IProjectTimeEditDTO
): Promise<IProjectTimeEditPOJO> => {
const { Time } = this.tenancy.models(tenantId);
// Validate customer existance.
const oldTime = await Time.query().findById(timeId).throwIfNotFound();
// Triggers `onProjectEdit` event.
await this.eventPublisher.emitAsync(events.projectTime.onEdit, {
tenantId,
oldTime,
timeDTO,
} as IProjectTimeEditEventPayload);
// Edits the given project under unit-of-work envirement.
return this.uow.withTransaction(tenantId, async (trx: Knex.Transaction) => {
// Triggers `onProjectEditing` event.
await this.eventPublisher.emitAsync(events.projectTime.onEditing, {
tenantId,
timeDTO,
oldTime,
trx,
} as IProjectTimeEditingEventPayload);
// Upsert the task's time object.
const time = await Time.query(trx).upsertGraphAndFetch({
id: timeId,
...timeDTO,
});
// Triggers `onProjectEdited` event.
await this.eventPublisher.emitAsync(events.projectTime.onEdited, {
tenantId,
oldTime,
timeDTO,
time,
trx,
} as IProjectTimeEditedEventPayload);
return time;
});
};
}

View File

@@ -0,0 +1,37 @@
import { IProjectTimeGetPOJO } from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { Inject, Service } from 'typedi';
import { TimeTransformer } from './TimeTransformer';
@Service()
export class GetTimeService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the tasks list.
* @param {number} tenantId - Tenant Id.
* @param {number} taskId - Task Id.
* @returns {Promise<IProjectTimeGetPOJO>}
*/
public getTime = async (
tenantId: number,
timeId: number
): Promise<IProjectTimeGetPOJO> => {
const { Time } = this.tenancy.models(tenantId);
// Retrieve the project.
const time = await Time.query()
.findById(timeId)
.withGraphFetched('project.contact')
.withGraphFetched('task')
.throwIfNotFound();
// Transformes and returns object.
return this.transformer.transform(tenantId, time, new TimeTransformer());
};
}

View File

@@ -0,0 +1,36 @@
import { Inject, Service } from 'typedi';
import { IProjectTimeGetPOJO } from '@/interfaces';
import { TransformerInjectable } from '@/lib/Transformer/TransformerInjectable';
import HasTenancyService from '@/services/Tenancy/TenancyService';
import { TimeTransformer } from './TimeTransformer';
@Service()
export class GetTimelineService {
@Inject()
private tenancy: HasTenancyService;
@Inject()
private transformer: TransformerInjectable;
/**
* Retrieve the tasks list.
* @param {number} tenantId - Tenant Id.
* @param {number} taskId - Task Id.
* @returns {Promise<IProjectTimeGetPOJO[]>}
*/
public getTimeline = async (
tenantId: number,
projectId: number
): Promise<IProjectTimeGetPOJO[]> => {
const { Time } = this.tenancy.models(tenantId);
// Retrieve the project.
const times = await Time.query()
.where('projectId', projectId)
.withGraphFetched('project.contact')
.withGraphFetched('task');
// Transformes and returns object.
return this.transformer.transform(tenantId, times, new TimeTransformer());
};
}

View File

@@ -0,0 +1,49 @@
import { Knex } from 'knex';
import { Inject, Service } from 'typedi';
import HasTenancyService from '@/services/Tenancy/TenancyService';
@Service()
export class SyncActualTimeTask {
@Inject()
private tenancy: HasTenancyService;
/**
* Increases the actual time of the given task.
* @param {number} tenantId
* @param {number} taskId
* @param {number} actualHours
* @param {Knex.Transaction} trx
*/
public increaseActualTimeTask = async (
tenantId: number,
taskId: number,
actualHours: number,
trx?: Knex.Transaction
) => {
const { Task } = this.tenancy.models(tenantId);
await Task.query(trx)
.findById(taskId)
.increment('actualHours', actualHours);
};
/**
* Decreases the actual time of the given task.
* @param {number} tenantId
* @param {number} taskId
* @param {number} actualHours
* @param {Knex.Transaction} trx
*/
public decreaseActualTimeTask = async (
tenantId: number,
taskId: number,
actualHours: number,
trx?: Knex.Transaction
) => {
const { Task } = this.tenancy.models(tenantId);
await Task.query(trx)
.findById(taskId)
.decrement('actualHours', actualHours);
};
}

View File

@@ -0,0 +1,91 @@
import { Inject, Service } from 'typedi';
import {
IProjectTimeCreatedEventPayload,
IProjectTimeDeletedEventPayload,
IProjectTimeEditedEventPayload,
} from '@/interfaces';
import events from '@/subscribers/events';
import { SyncActualTimeTask } from './SyncActualTimeTask';
@Service()
export class SyncActualTimeTaskSubscriber {
@Inject()
private syncActualTimeTask: SyncActualTimeTask;
/**
* Attaches events with handlers.
* @param bus
*/
attach(bus) {
bus.subscribe(
events.projectTime.onCreated,
this.handleIncreaseActualTimeOnTimeCreate
);
bus.subscribe(
events.projectTime.onDeleted,
this.handleDecreaseActaulTimeOnTimeDelete
);
bus.subscribe(
events.projectTime.onEdited,
this.handleAdjustActualTimeOnTimeEdited
);
}
/**
* Handles increasing the actual time of the task once time entry be created.
* @param {IProjectTimeCreatedEventPayload} payload -
*/
private handleIncreaseActualTimeOnTimeCreate = async ({
tenantId,
time,
trx,
}: IProjectTimeCreatedEventPayload) => {
await this.syncActualTimeTask.increaseActualTimeTask(
tenantId,
time.taskId,
time.duration,
trx
);
};
/**
* Handle decreasing the actual time of the tsak once time entry be deleted.
* @param {IProjectTimeDeletedEventPayload} payload
*/
private handleDecreaseActaulTimeOnTimeDelete = async ({
tenantId,
oldTime,
trx,
}: IProjectTimeDeletedEventPayload) => {
await this.syncActualTimeTask.decreaseActualTimeTask(
tenantId,
oldTime.taskId,
oldTime.duration,
trx
);
};
/**
* Handle adjusting the actual time of the task once time be edited.
* @param {IProjectTimeEditedEventPayload} payload -
*/
private handleAdjustActualTimeOnTimeEdited = async ({
tenantId,
time,
oldTime,
trx,
}: IProjectTimeEditedEventPayload) => {
await this.syncActualTimeTask.decreaseActualTimeTask(
tenantId,
oldTime.taskId,
oldTime.duration,
trx
);
await this.syncActualTimeTask.increaseActualTimeTask(
tenantId,
time.taskId,
time.duration,
trx
);
};
}

View File

@@ -0,0 +1,57 @@
import { Transformer } from '@/lib/Transformer/Transformer';
import Time from 'models/Time';
import { formatMinutes } from 'utils/formatMinutes';
export class TimeTransformer extends Transformer {
/**
* Include these attributes to sale invoice object.
* @returns {Array}
*/
public includeAttributes = (): string[] => {
return ['projectName', 'taskName', 'customerName', 'durationFormatted'];
};
/**
* Exclude attributes.
* @returns {string[]}
*/
public excludeAttributes = (): string[] => {
return ['project', 'task'];
};
/**
* Retrieves the project name that associated to the time entry.
* @param {Time} time
* @returns {string}
*/
public projectName = (time: Time) => {
return time.project.name;
};
/**
* Retrieves the task name that associated to the time entry.
* @param {Time} time
* @returns {string}
*/
public taskName = (time: Time) => {
return time.task.name;
};
/**
* Retrieves the customer name that associated to the task of the time entry.
* @param {Time} time
* @returns {string}
*/
public customerName = (time: Time) => {
return time?.project?.contact?.displayName;
};
/**
* Retrieves the formatted duration.
* @param {Time} time
* @returns {string}
*/
public durationFormatted = (time: Time) => {
return formatMinutes(time.duration);
}
}

View File

@@ -0,0 +1,96 @@
import { Inject, Service } from 'typedi';
import { CreateTimeService } from './CreateTime';
import { EditTimeService } from './EditTime';
import { GetTimelineService } from './GetTimes';
import { GetTimeService } from './GetTime';
import { DeleteTimeService } from './DeleteTime';
import {
IProjectTimeCreateDTO,
IProjectTimeCreatePOJO,
IProjectTimeEditDTO,
IProjectTimeEditPOJO,
IProjectTimeGetPOJO,
} from '@/interfaces';
@Service()
export class TimesApplication {
@Inject()
private createTimeService: CreateTimeService;
@Inject()
private editTimeService: EditTimeService;
@Inject()
private deleteTimeService: DeleteTimeService;
@Inject()
private getTimeService: GetTimeService;
@Inject()
private getTimelineService: GetTimelineService;
/**
* Creates a new time for specific project's task.
* @param {number} tenantId - Tenant id.
* @param {IProjectTimeCreateDTO} timeDTO - Create project's time DTO.
* @return {Promise<IProjectTimeCreatePOJO>}
*/
public createTime = (
tenantId: number,
taskId: number,
timeDTO: IProjectTimeCreateDTO
): Promise<IProjectTimeCreatePOJO> => {
return this.createTimeService.createTime(tenantId, taskId, timeDTO);
};
/**
* Edits details of the given task.
* @param {number} tenantId - Tenant id.
* @param {number} vendorId - Vendor id.
* @param {IProjectCreateDTO} projectDTO - Create project DTO.
* @returns {Promise<IProjectTimeEditPOJO>}
*/
public editTime = (
tenantId: number,
timeId: number,
taskDTO: IProjectTimeEditDTO
): Promise<IProjectTimeEditPOJO> => {
return this.editTimeService.editTime(tenantId, timeId, taskDTO);
};
/**
* Deletes the given task.
* @param {number} tenantId
* @param {number} taskId
* @return {Promise<void>}
*/
public deleteTime = (tenantId: number, timeId: number): Promise<void> => {
return this.deleteTimeService.deleteTime(tenantId, timeId);
};
/**
* Retrieves the given task details.
* @param {number} tenantId
* @param {number} timeId
* @returns {Promise<IProjectTimeGetPOJO>}
*/
public getTime = (
tenantId: number,
timeId: number
): Promise<IProjectTimeGetPOJO> => {
return this.getTimeService.getTime(tenantId, timeId);
};
/**
* Retrieves the vendors paginated list.
* @param {number} tenantId
* @param {IVendorsFilter} filterDTO
* @returns {Promise<IProjectTimeGetPOJO[]>}
*/
public getTimeline = (
tenantId: number,
projectId: number
): Promise<IProjectTimeGetPOJO[]> => {
return this.getTimelineService.getTimeline(tenantId, projectId);
};
}