fix billing.

This commit is contained in:
Simon Larsen
2022-11-24 15:10:21 +00:00
parent 2041535537
commit 369837eaa3
25 changed files with 876 additions and 65 deletions

View File

@@ -77,6 +77,8 @@ export default class BaseModel extends BaseEntity {
public updateBillingPlan!: PlanSelect | null;
public deleteBillingPlan!: PlanSelect | null;
public allowAccessIfSubscriptionIsUnpaid!: boolean;
public currentUserCanAccessColumnBy!: string | null;
public labelsColumn!: string | null;

View File

@@ -6,7 +6,7 @@ export default class Route {
}
public set route(v: string) {
const matchRouteCharacters: RegExp =
/^[a-zA-Z\d\-!#$&'()*+,./:;=?@[\]]*$/;
/^[a-zA-Z_\d\-!#$&'()*+,./:;=?@[\]]*$/;
if (v && !matchRouteCharacters.test(v)) {
throw new BadDataException(`Invalid route: ${v}`);
}

View File

@@ -177,4 +177,12 @@ export default class SubscriptionPlan {
return true;
}
public static isUnpaid(subscriptionStatus: string): boolean {
if (subscriptionStatus === "incomplete" || subscriptionStatus === "incomplete_expired" || subscriptionStatus === "past_due" || subscriptionStatus === "canceled" || subscriptionStatus === "unpaid") {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,5 @@
export default () => {
return (ctr: Function) => {
ctr.prototype.allowAccessIfSubscriptionIsUnpaid = true;
};
};

View File

@@ -26,6 +26,7 @@ enum ColumnType {
PositiveNumber = 'integer',
BigPositiveNumber = 'bigint',
SmallNumber = 'smallint',
Decimal = 'decimal',
Number = 'integer',
BigNumber = 'bigint',
Markdown = 'text',

View File

@@ -19,4 +19,5 @@ export default interface DatabaseCommonInteractionProps {
isMultiTenantRequest?: boolean | undefined;
ignoreHooks?: boolean | undefined;
currentPlan?: PlanSelect | undefined;
isSubscriptionUnpaid?: boolean | undefined;
}

View File

@@ -183,6 +183,18 @@ enum Permission {
CanDeleteIncidentPublicNote = 'CanDeleteIncidentPublicNote',
CanReadIncidentPublicNote = 'CanReadIncidentPublicNote',
CanCreateInvoices = 'CanCreateInvoices',
CanEditInvoices = 'CanEditInvoices',
CanDeleteInvoices = 'CanDeleteInvoices',
CanReadInvoices = 'CanReadInvoices',
CanCreateBillingPaymentMethod = 'CanCreateBillingPaymentMethod',
CanEditBillingPaymentMethod = 'CanEditBillingPaymentMethod',
CanDeleteBillingPaymentMethod = 'CanDeleteBillingPaymentMethod',
CanReadBillingPaymentMethod = 'CanReadBillingPaymentMethod',
CanCreateProjectMonitor = 'CanCreateProjectMonitor',
CanEditProjectMonitor = 'CanEditProjectMonitor',
CanDeleteProjectMonitor = 'CanDeleteProjectMonitor',
@@ -1108,6 +1120,75 @@ export class PermissionHelper {
isAccessControlPermission: false,
},
{
permission: Permission.CanCreateInvoices,
title: 'Can Create Invoices',
description:
'A user assigned this permission can create Invoices this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanDeleteInvoices,
title: 'Can Delete Invoices',
description:
'A user assigned this permission can delete Invoices of this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanEditInvoices,
title: 'Can Edit Invoices',
description:
'A user assigned this permission can edit Invoices of this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanReadInvoices,
title: 'Can Read Invoices',
description:
'A user assigned this permission can read Invoices of this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanCreateBillingPaymentMethod,
title: 'Can Create Payment Method',
description:
'A user assigned this permission can create Payment Method this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanDeleteBillingPaymentMethod,
title: 'Can Delete Payment Method',
description:
'A user assigned this permission can delete Payment Method of this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanEditBillingPaymentMethod,
title: 'Can Edit Payment Method',
description:
'A user assigned this permission can edit Payment Method of this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanReadBillingPaymentMethod,
title: 'Can Read Payment Method',
description:
'A user assigned this permission can read Payment Method of this project.',
isAssignableToTenant: true,
isAccessControlPermission: false,
},
{
permission: Permission.CanCreateProjectOnCallDuty,
title: 'Can Create On-Call Duty',

View File

@@ -216,7 +216,9 @@ export default class BaseAPI<
}
if (IsBillingEnabled && props.tenantId) {
props.currentPlan = await ProjectService.getCurrentPlan(props.tenantId) || undefined;
const plan = await ProjectService.getCurrentPlan(props.tenantId!);
props.currentPlan = plan.plan || undefined;
props.isSubscriptionUnpaid = plan.isSubscriptionUnpaid;
}
return props;

View File

@@ -0,0 +1,159 @@
import BaseModel from 'Common/Models/BaseModel';
import BadDataException from 'Common/Types/Exception/BadDataException';
import { JSONObject } from 'Common/Types/JSON';
import Permission from 'Common/Types/Permission';
import BillingInvoice from 'Model/Models/BillingInvoice';
import Project from 'Model/Models/Project';
import { IsBillingEnabled } from '../Config';
import UserMiddleware from '../Middleware/UserAuthorization';
import BillingInvoiceService, {
Service as BillingInvoiceServiceType,
} from '../Services/BillingInvoiceService';
import BillingService, { Invoice } from '../Services/BillingService';
import ProjectService from '../Services/ProjectService';
import {
ExpressRequest,
ExpressResponse,
NextFunction,
} from '../Utils/Express';
import Response from '../Utils/Response';
import BaseAPI from './BaseAPI';
export default class UserAPI extends BaseAPI<
BillingInvoice,
BillingInvoiceServiceType
> {
public constructor() {
super(BillingInvoice, BillingInvoiceService);
this.router.post(
`/${new this.entityType().getCrudApiPath()?.toString()}/pay`,
UserMiddleware.getUserMiddleware,
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction
) => {
try {
if (!IsBillingEnabled) {
throw new BadDataException(
'Billign is not enabled for this server'
);
}
if (req.body['projectId']) {
throw new BadDataException(
'projectId is required in request body'
);
}
const userPermissions = (await this.getPermissionsForTenant(
req
)).filter((permission) => {
console.log(permission.permission);
//FIX: Change "Project"
return (
permission.permission.toString() === Permission.ProjectOwner.toString() || permission.permission.toString() === Permission.CanEditInvoices.toString()
);
});
if (userPermissions.length === 0) {
throw new BadDataException(
`You need ${Permission.ProjectOwner} or ${Permission.CanEditInvoices} permission to pay invoices.`
);
}
const project: Project | null =
await ProjectService.findOneById({
id: this.getTenantId(req)!,
props: {
isRoot: true,
},
select: {
_id: true,
paymentProviderCustomerId: true,
paymentProviderSubscriptionId: true
},
});
if (!project) {
throw new BadDataException('Project not found');
}
if (!project) {
throw new BadDataException('Project not found');
}
if (!project.paymentProviderCustomerId) {
throw new BadDataException(
'Payment Provider customer not found'
);
}
if (!project.paymentProviderSubscriptionId) {
throw new BadDataException(
'Payment Provider subscription not found'
);
}
const body: JSONObject = req.body;
const item: BillingInvoice = BaseModel.fromJSON<BillingInvoice>(
body['data'] as JSONObject,
this.entityType
) as BillingInvoice;
if (!item.paymentProviderInvoiceId) {
throw new BadDataException("Invoice ID not found");
}
if (!item.paymentProviderCustomerId) {
throw new BadDataException("Customer ID not found");
}
const invoice: Invoice =
await BillingService.payInvoice(
item.paymentProviderCustomerId!,
item.paymentProviderInvoiceId!,
);
// save updated status.
await this.service.updateOneBy({
query: {
paymentProviderInvoiceId: invoice.id!
},
props: {
isRoot: true,
ignoreHooks: true
},
data: {
status: invoice.status
}
})
// refresh subscription status.
const subscriptionState = await BillingService.getSubscriptionStatus(project.paymentProviderSubscriptionId as string);
await ProjectService.updateOneById({
id: project.id!,
data: {
paymentProviderSubscriptionStatus: subscriptionState
},
props: {
isRoot: true,
ignoreHooks: true
}
});
return Response.sendEmptyResponse(req, res);
} catch (err) {
next(err);
}
}
);
}
}

View File

@@ -1,4 +1,5 @@
import BadDataException from 'Common/Types/Exception/BadDataException';
import Permission from 'Common/Types/Permission';
import BillingPaymentMethod from 'Model/Models/BillingPaymentMethod';
import Project from 'Model/Models/Project';
import { IsBillingEnabled } from '../Config';
@@ -50,7 +51,7 @@ export default class UserAPI extends BaseAPI<
console.log(permission.permission);
//FIX: Change "Project"
return (
permission.permission.toString() === 'ProjectOwner'
permission.permission.toString() === Permission.ProjectOwner.toString() || permission.permission.toString() === Permission.CanCreateBillingPaymentMethod.toString()
);
});

View File

@@ -12,9 +12,6 @@ import ApiKey from 'Model/Models/ApiKey';
import { LessThan } from 'typeorm';
import OneUptimeDate from 'Common/Types/Date';
import UserType from 'Common/Types/UserType';
import { PlanSelect } from 'Common/Types/Billing/SubscriptionPlan';
import { IsBillingEnabled } from '../Config';
import ProjectService from '../Services/ProjectService';
export default class ProjectMiddleware {
public static getProjectId(req: ExpressRequest): ObjectID | null {
@@ -33,20 +30,6 @@ export default class ProjectMiddleware {
return projectId;
}
public static async getProjectPlan(req: ExpressRequest): Promise<PlanSelect | null> {
if (!IsBillingEnabled) {
return null;
}
const projectId = this.getProjectId(req);
if (!projectId) {
return null;
}
return await ProjectService.getCurrentPlan(projectId);
}
public static getApiKey(req: ExpressRequest): ObjectID | null {
let apiKey: ObjectID | null = null;

View File

@@ -0,0 +1,92 @@
import PostgresDatabase from '../Infrastructure/PostgresDatabase';
import Model from 'Model/Models/BillingInvoice';
import DatabaseService, { OnDelete, OnFind } from './DatabaseService';
import FindBy from '../Types/Database/FindBy';
import ProjectService from './ProjectService';
import BadDataException from 'Common/Types/Exception/BadDataException';
import Project from 'Model/Models/Project';
import BillingService from './BillingService';
import DeleteBy from '../Types/Database/DeleteBy';
import URL from 'Common/Types/API/URL';
export class Service extends DatabaseService<Model> {
public constructor(postgresDatabase?: PostgresDatabase) {
super(Model, postgresDatabase);
}
protected override async onBeforeFind(
findBy: FindBy<Model>
): Promise<OnFind<Model>> {
if (!findBy.props.tenantId) {
throw new BadDataException('ProjectID not found.');
}
const project: Project | null = await ProjectService.findOneById({
id: findBy.props.tenantId!,
props: {
...findBy.props,
isRoot: true,
ignoreHooks: true,
},
select: {
_id: true,
paymentProviderCustomerId: true,
},
});
if (!project) {
throw new BadDataException('Project not found');
}
if (!project.paymentProviderCustomerId) {
throw new BadDataException(
'Payment provider customer id not found.'
);
}
const invoices = await BillingService.getInvoices(
project.paymentProviderCustomerId
);
await this.deleteBy({
query: {
projectId: findBy.props.tenantId!,
},
props: {
isRoot: true,
ignoreHooks: true,
},
});
for (const invoice of invoices) {
const billingInvoice = new Model();
billingInvoice.projectId = project.id!;
billingInvoice.amount = invoice.amount;
billingInvoice.downloadableLink = URL.fromString(invoice.downloadableLink);
billingInvoice.currencyCode = invoice.currencyCode;
billingInvoice.paymentProviderCustomerId = invoice.customerId || '';
billingInvoice.paymentProviderSubscriptionId = invoice.subscriptionId || '';
billingInvoice.status = invoice.status || '';
billingInvoice.paymentProviderInvoiceId = invoice.id;
await this.create({
data: billingInvoice,
props: {
isRoot: true,
},
});
}
return { findBy, carryForward: invoices };
}
protected override async onBeforeDelete(
_deleteBy: DeleteBy<Model>
): Promise<OnDelete<Model>> {
throw new BadDataException("Invoice should not be deleted.")
}
}
export default new Service();

View File

@@ -13,6 +13,16 @@ export interface PaymentMethod {
isDefault: boolean;
}
export interface Invoice {
id: string;
amount: number;
currencyCode: string;
subscriptionId?: string | undefined;
status: string;
downloadableLink: string;
customerId: string | undefined;
}
export class BillingService {
private static stripe: Stripe = new Stripe(BillingPrivateKey, {
apiVersion: '2022-08-01',
@@ -335,6 +345,51 @@ export class BillingService {
return subscription.status;
}
public static async getInvoices(customerId: string): Promise<Array<Invoice>> {
const invoices = await this.stripe.invoices.list({
customer: customerId,
limit: 100,
});
return invoices.data.map((invoice) => {
return {
id: invoice.id!,
amount: invoice.amount_due,
currencyCode: invoice.currency,
subscriptionId: invoice.subscription?.toString() || undefined,
status: invoice.status?.toString() || 'Unknown',
downloadableLink: invoice.invoice_pdf?.toString() || '',
customerId: invoice.customer?.toString() || ''
}
});
}
public static async payInvoice(customerId: string, invoiceId: string): Promise<Invoice> {
// after the invoice is paid, // please fetch subscription and check the status.
const paymentMethods = await this.getPaymentMethods(customerId);
if (paymentMethods.length === 0) {
throw new BadDataException("Payment Method not added. Please add a payment method.");
}
const invoice = await this.stripe.invoices.pay(
invoiceId, {
payment_method: paymentMethods[0]?.id || ''
}
);
return {
id: invoice.id!,
amount: invoice.amount_due,
currencyCode: invoice.currency,
subscriptionId: invoice.subscription?.toString() || undefined,
status: invoice.status?.toString() || 'Unknown',
downloadableLink: invoice.invoice_pdf?.toString() || '',
customerId: invoice.customer?.toString() || ''
}
}
}
export default BillingService;

View File

@@ -146,7 +146,7 @@ export class Service extends DatabaseService<Model> {
plan,
project.paymentProviderSubscriptionSeats as number,
plan.getYearlyPlanId() ===
updateBy.data.paymentProviderPlanId,
updateBy.data.paymentProviderPlanId,
project.trialEndsAt
);
}
@@ -429,7 +429,7 @@ export class Service extends DatabaseService<Model> {
let ownerTeam: Team = new Team();
ownerTeam.projectId = createdItem.id!;
ownerTeam.name = 'Owners';
ownerTeam.shouldHaveAtleastOneMember = true;
ownerTeam.shouldHaveAtleastOneMember = true;
ownerTeam.isPermissionsEditable = false;
ownerTeam.isTeamEditable = false;
ownerTeam.isTeamDeleteable = false;
@@ -592,15 +592,16 @@ export class Service extends DatabaseService<Model> {
return onDelete;
}
public async getCurrentPlan(projectId: ObjectID): Promise<PlanSelect | null> {
public async getCurrentPlan(projectId: ObjectID): Promise<{ plan: PlanSelect | null, isSubscriptionUnpaid: boolean }> {
if (!IsBillingEnabled) {
return null;
return { plan: null, isSubscriptionUnpaid: false };
}
const project = await this.findOneById({
id: projectId,
select: {
paymentProviderPlanId: true
paymentProviderPlanId: true,
paymentProviderSubscriptionStatus: true
},
props: {
isRoot: true,
@@ -616,7 +617,7 @@ export class Service extends DatabaseService<Model> {
throw new BadDataException("Project does not have any plans");
}
return SubscriptionPlan.getPlanSelect(project.paymentProviderPlanId);
return { plan: SubscriptionPlan.getPlanSelect(project.paymentProviderPlanId), isSubscriptionUnpaid: SubscriptionPlan.isUnpaid(project.paymentProviderSubscriptionStatus || 'active')};
}
}

View File

@@ -964,6 +964,10 @@ export default class ModelPermission {
const model = new modelType();
if (props.isSubscriptionUnpaid && !model.allowAccessIfSubscriptionIsUnpaid) {
throw new PaymentRequiredException("Your current subscription is in an unpaid state. Looks like your payment method failed. Please add a new payment method in Project Settings > Billing to proceed.")
}
if (type === DatabaseRequestType.Create && model.createBillingPlan) {
if (!SubscriptionPlan.isFeatureAccessibleOnCurrentPlan(model.createBillingPlan, props.currentPlan)) {
throw new PaymentRequiredException("Please upgrade your plan to " + model.createBillingPlan + " to access this feature");

View File

@@ -58,7 +58,8 @@ import {
ExternalLink,
Layers,
Codesandbox,
Star
Star,
ArrowDown
} from 'react-feather';
export enum SizeProp {
@@ -144,7 +145,8 @@ export enum IconProp {
Clock,
Invoice,
Upgrade,
Star
Star,
Download
}
export interface ComponentProps {
@@ -646,6 +648,14 @@ const Icon: FunctionComponent<ComponentProps> = ({
color={color ? color.toString() : (undefined as any)}
/>
)}
{icon === IconProp.Download && (
<ArrowDown
size={size}
strokeWidth={thick ? thick : ''}
color={color ? color.toString() : (undefined as any)}
/>
)}
</div>
);
};

View File

@@ -338,9 +338,6 @@ const ModelTable: Function = <TBaseModel extends BaseModel>(
};
const fetchItems: Function = async () => {
if (isLoading) {
return;
}
setError('');
setIsLoading(true);

View File

@@ -1,17 +1,74 @@
import Route from 'Common/Types/API/Route';
import { JSONObject } from 'Common/Types/JSON';
import Button, { ButtonStyleType } from 'CommonUI/src/Components/Button/Button';
import { IconProp } from 'CommonUI/src/Components/Icon/Icon';
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
import Page from 'CommonUI/src/Components/Page/Page';
import React, { FunctionComponent, ReactElement } from 'react';
import Navigation from 'CommonUI/src/Utils/Navigation';
import React, {
FunctionComponent,
ReactElement,
useState,
} from 'react';
import Text from 'Common/Types/Text';
import PageMap from '../../Utils/PageMap';
import RouteMap from '../../Utils/RouteMap';
import PageComponentProps from '../PageComponentProps';
import DashboardSideMenu from './SideMenu';
import Alert, { AlertType } from 'CommonUI/src/Components/Alerts/Alert';
import BillingInvoice from 'Model/Models/BillingInvoice';
import FieldType from 'CommonUI/src/Components/Types/FieldType';
import URL from 'Common/Types/API/URL';
import Pill from 'CommonUI/src/Components/Pill/Pill';
import { Green, Yellow } from 'Common/Types/BrandColors';
import { DASHBOARD_API_URL } from 'CommonUI/src/Config';
import BaseAPI from 'CommonUI/src/Utils/API/API';
import ModelAPI from 'CommonUI/src/Utils/ModelAPI/ModelAPI';
import HTTPErrorResponse from 'Common/Types/API/HTTPErrorResponse';
import ConfirmModal from 'CommonUI/src/Components/Modal/ConfirmModal';
import ComponentLoader from 'CommonUI/src/Components/ComponentLoader/ComponentLoader';
export interface ComponentProps extends PageComponentProps {}
export interface ComponentProps extends PageComponentProps { }
const Settings: FunctionComponent<ComponentProps> = (
_props: ComponentProps
props: ComponentProps
): ReactElement => {
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState<boolean>(false);
const payInvoice = async (customerId: string, invoiceId: string) => {
try {
setIsLoading(true);
await BaseAPI.post<JSONObject>(
URL.fromString(DASHBOARD_API_URL.toString()).addRoute(
`/billing-invoices/pay`
),
{
data: {
paymentProviderInvoiceId: invoiceId,
paymentProviderCustomerId: customerId,
}
},
ModelAPI.getCommonHeaders()
);
Navigation.reload();
} catch (err) {
try {
setError(
(err as HTTPErrorResponse).message ||
'Server Error. Please try again'
);
} catch (e) {
setError('Server Error. Please try again');
}
setIsLoading(false);
}
};
return (
<Page
title={'Project Settings'}
@@ -31,11 +88,105 @@ const Settings: FunctionComponent<ComponentProps> = (
]}
sideMenu={<DashboardSideMenu />}
>
<Alert
type={AlertType.DANGER}
strongTitle="DANGER ZONE"
title="Deleting your project will delete it permanently and there is no way to recover. "
/>
{isLoading ? <ComponentLoader /> : <></>}
{!isLoading ? <ModelTable<BillingInvoice>
modelType={BillingInvoice}
id="invoices-table"
isDeleteable={false}
isEditable={false}
isCreateable={false}
isViewable={false}
cardProps={{
icon: IconProp.File,
title: 'Invoices',
description:
'Here is a list of invoices for this project.',
}}
noItemsMessage={'No invoices so far.'}
query={{
projectId: props.currentProject?._id,
}}
showRefreshButton={true}
showFilterButton={false}
selectMoreFields={{
currencyCode: true,
paymentProviderCustomerId: true
}}
columns={[
{
field: {
paymentProviderInvoiceId: true,
},
title: 'Invoice ID',
type: FieldType.Text,
},
{
field: {
amount: true,
},
title: 'Amount',
type: FieldType.Text,
isFilterable: true,
getElement: (item: JSONObject) => {
return <span>{`${(item['amount'] as number) / 100} ${item['currencyCode']?.toString().toUpperCase()}`}</span>
}
},
{
field: {
status: true,
},
title: 'Invoice Status',
type: FieldType.Text,
isFilterable: true,
getElement: (item: JSONObject) => {
if (item['status'] === "paid") {
return <Pill text={Text.uppercaseFirstLetter(item['status'] as string)} color={Green} />
} else {
return <Pill text={Text.uppercaseFirstLetter(item['status'] as string)} color={Yellow} />
}
}
},
{
field: {
downloadableLink: true,
},
title: 'Actions',
type: FieldType.Text,
isFilterable: true,
getElement: (item: JSONObject) => {
return (
<div>
{item['downloadableLink'] ? <Button icon={IconProp.Download} onClick={() => {
Navigation.navigate(item['downloadableLink'] as URL);
}} title="Download" /> : <></>}
{item['status'] !== "paid" ? <Button icon={IconProp.Billing} onClick={() => {
payInvoice(item['paymentProviderCustomerId'] as string, item['paymentProviderInvoiceId'] as string);
}} title="Pay Invoice" /> : <></>}
</div>
)
}
},
]}
/> : <></>}
{error ? (
<ConfirmModal
title={`Error`}
description={`${error}`}
submitButtonText={'Close'}
onSubmit={() => {
setError('');
}}
submitButtonType={ButtonStyleType.NORMAL}
/>
): <></>}
</Page>
);
};

View File

@@ -172,15 +172,15 @@ const DashboardSideMenu: FunctionComponent = (): ReactElement => {
}}
icon={IconProp.Billing}
/>
{/* <SideMenuItem
<SideMenuItem
link={{
title: 'Invoices',
to: RouteUtil.populateRouteParams(
RouteMap[PageMap.SETTINGS_BILLING_INVOICES] as Route
),
}}
icon={IconProp.File}
/> */}
icon={IconProp.TextFile}
/>
</SideMenuSection>
) : (
<></>

View File

@@ -13,6 +13,8 @@ import UserService, {
import BillingPaymentMethodAPI from 'CommonServer/API/BillingPaymentMethodAPI';
import BillingInvoiceAPI from 'CommonServer/API/BillingInvoiceAPI';
import Project from 'Model/Models/Project';
import ProjectService, {
Service as ProjectServiceType,
@@ -390,6 +392,7 @@ app.use(
app.use(new StatusPageAPI().getRouter());
app.use(new BillingPaymentMethodAPI().getRouter());
app.use(new BillingInvoiceAPI().getRouter());
app.use(
new BaseAPI<

View File

@@ -0,0 +1,246 @@
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import BaseModel from 'Common/Models/BaseModel';
import User from './User';
import Project from './Project';
import CrudApiEndpoint from 'Common/Types/Database/CrudApiEndpoint';
import Route from 'Common/Types/API/Route';
import TableColumnType from 'Common/Types/Database/TableColumnType';
import TableColumn from 'Common/Types/Database/TableColumn';
import ColumnType from 'Common/Types/Database/ColumnType';
import ObjectID from 'Common/Types/ObjectID';
import ColumnLength from 'Common/Types/Database/ColumnLength';
import TableAccessControl from 'Common/Types/Database/AccessControl/TableAccessControl';
import Permission from 'Common/Types/Permission';
import ColumnAccessControl from 'Common/Types/Database/AccessControl/ColumnAccessControl';
import TenantColumn from 'Common/Types/Database/TenantColumn';
import SingularPluralName from 'Common/Types/Database/SingularPluralName';
import AllowAccessIfSubscriptionIsUnpaid from 'Common/Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid';
import URL from 'Common/Types/API/URL';
@AllowAccessIfSubscriptionIsUnpaid()
@TenantColumn('projectId')
@TableAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
delete: [],
update: [],
})
@CrudApiEndpoint(new Route('/billing-invoices'))
@SingularPluralName('Invoice', 'Invoices')
@Entity({
name: 'BillingInvoice',
})
export default class BillingInvoice extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({
manyToOneRelationColumn: 'projectId',
type: TableColumnType.Entity,
modelType: Project,
})
@ManyToOne(
(_type: string) => {
return Project;
},
{
eager: false,
nullable: true,
onDelete: 'CASCADE',
orphanedRowAction: 'nullify',
}
)
@JoinColumn({ name: 'projectId' })
public project?: Project = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@Index()
@TableColumn({
type: TableColumnType.ObjectID,
required: true,
canReadOnPopulate: true,
})
@Column({
type: ColumnType.ObjectID,
nullable: false,
transformer: ObjectID.getDatabaseTransformer(),
})
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({
manyToOneRelationColumn: 'createdByUserId',
type: TableColumnType.Entity,
modelType: User,
})
@ManyToOne(
(_type: string) => {
return User;
},
{
eager: false,
nullable: true,
onDelete: 'CASCADE',
orphanedRowAction: 'nullify',
}
)
@JoinColumn({ name: 'createdByUserId' })
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({ type: TableColumnType.ObjectID })
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public createdByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({
manyToOneRelationColumn: 'deletedByUserId',
type: TableColumnType.ObjectID,
})
@ManyToOne(
(_type: string) => {
return User;
},
{
cascade: false,
eager: false,
nullable: true,
onDelete: 'CASCADE',
orphanedRowAction: 'nullify',
}
)
@JoinColumn({ name: 'deletedByUserId' })
public deletedByUser?: User = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({ type: TableColumnType.ObjectID })
@Column({
type: ColumnType.ObjectID,
nullable: true,
transformer: ObjectID.getDatabaseTransformer(),
})
public deletedByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({ type: TableColumnType.Number })
@Column({
type: ColumnType.Decimal,
nullable: false,
unique: false,
})
public amount?: number = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: false,
unique: false,
})
public currencyCode?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({ type: TableColumnType.LongURL })
@Column({
type: ColumnType.LongURL,
nullable: false,
unique: false,
transformer: URL.getDatabaseTransformer(),
})
public downloadableLink?: URL = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: false,
unique: false,
})
public status?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: false,
unique: false,
})
public paymentProviderCustomerId?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: true,
unique: false,
})
public paymentProviderSubscriptionId?: string = undefined;
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner, Permission.CanReadInvoices],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@Column({
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
nullable: false,
unique: false,
})
public paymentProviderInvoiceId?: string = undefined;
}

View File

@@ -14,13 +14,15 @@ import Permission from 'Common/Types/Permission';
import ColumnAccessControl from 'Common/Types/Database/AccessControl/ColumnAccessControl';
import TenantColumn from 'Common/Types/Database/TenantColumn';
import SingularPluralName from 'Common/Types/Database/SingularPluralName';
import AllowAccessIfSubscriptionIsUnpaid from 'Common/Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid';
@AllowAccessIfSubscriptionIsUnpaid()
@TenantColumn('projectId')
@TableAccessControl({
create: [Permission.ProjectOwner],
read: [Permission.ProjectOwner],
delete: [Permission.ProjectOwner],
update: [Permission.ProjectOwner],
create: [Permission.ProjectOwner, Permission.CanCreateBillingPaymentMethod],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
delete: [Permission.ProjectOwner, Permission.CanDeleteBillingPaymentMethod],
update: [],
})
@CrudApiEndpoint(new Route('/billing-payment-methods'))
@SingularPluralName('Payment Method', 'Payment Methods')
@@ -29,8 +31,8 @@ import SingularPluralName from 'Common/Types/Database/SingularPluralName';
})
export default class BillingPaymentMethod extends BaseModel {
@ColumnAccessControl({
create: [Permission.ProjectOwner],
read: [Permission.ProjectOwner],
create: [Permission.ProjectOwner, Permission.CanCreateBillingPaymentMethod],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({
@@ -53,8 +55,8 @@ export default class BillingPaymentMethod extends BaseModel {
public project?: Project = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner],
read: [Permission.ProjectOwner],
create: [Permission.ProjectOwner, Permission.CanCreateBillingPaymentMethod],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@Index()
@@ -71,8 +73,8 @@ export default class BillingPaymentMethod extends BaseModel {
public projectId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner],
read: [Permission.ProjectOwner],
create: [Permission.ProjectOwner, Permission.CanCreateBillingPaymentMethod],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({
@@ -95,8 +97,8 @@ export default class BillingPaymentMethod extends BaseModel {
public createdByUser?: User = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner],
read: [Permission.ProjectOwner],
create: [Permission.ProjectOwner, Permission.CanCreateBillingPaymentMethod],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({ type: TableColumnType.ObjectID })
@@ -109,7 +111,7 @@ export default class BillingPaymentMethod extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({
@@ -133,7 +135,7 @@ export default class BillingPaymentMethod extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({ type: TableColumnType.ObjectID })
@@ -145,8 +147,8 @@ export default class BillingPaymentMethod extends BaseModel {
public deletedByUserId?: ObjectID = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner],
read: [Permission.ProjectOwner],
create: [Permission.ProjectOwner, Permission.CanCreateBillingPaymentMethod],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@@ -160,7 +162,7 @@ export default class BillingPaymentMethod extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@@ -174,7 +176,7 @@ export default class BillingPaymentMethod extends BaseModel {
@ColumnAccessControl({
create: [],
read: [Permission.ProjectOwner],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@@ -187,8 +189,8 @@ export default class BillingPaymentMethod extends BaseModel {
public paymentProviderCustomerId?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner],
read: [Permission.ProjectOwner],
create: [Permission.ProjectOwner, Permission.CanCreateBillingPaymentMethod],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({ type: TableColumnType.ShortText })
@@ -201,8 +203,8 @@ export default class BillingPaymentMethod extends BaseModel {
public last4Digits?: string = undefined;
@ColumnAccessControl({
create: [Permission.ProjectOwner],
read: [Permission.ProjectOwner],
create: [Permission.ProjectOwner, Permission.CanCreateBillingPaymentMethod],
read: [Permission.ProjectOwner, Permission.CanReadBillingPaymentMethod],
update: [],
})
@TableColumn({ type: TableColumnType.Boolean })

View File

@@ -57,6 +57,7 @@ import ProjectSmtpConfig from './ProjectSmtpConfig';
import Domain from './Domain';
import File from './File';
import BillingInvoice from './BillingInvoice';
export default [
User,
@@ -100,4 +101,5 @@ export default [
ScheduledMaintenanceInternalNote,
BillingPaymentMethods,
BillingInvoice
];

View File

@@ -11,12 +11,14 @@ import Route from 'Common/Types/API/Route';
import TableColumnType from 'Common/Types/Database/TableColumnType';
import SlugifyColumn from 'Common/Types/Database/SlugifyColumn';
import TableAccessControl from 'Common/Types/Database/AccessControl/TableAccessControl';
import AllowAccessIfSubscriptionIsUnpaid from 'Common/Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid';
import Permission from 'Common/Types/Permission';
import ColumnAccessControl from 'Common/Types/Database/AccessControl/ColumnAccessControl';
import TenantColumn from 'Common/Types/Database/TenantColumn';
import SingularPluralName from 'Common/Types/Database/SingularPluralName';
import MultiTenentQueryAllowed from 'Common/Types/Database/MultiTenentQueryAllowed';
@AllowAccessIfSubscriptionIsUnpaid()
@MultiTenentQueryAllowed(true)
@TableAccessControl({
create: [Permission.User],

View File

@@ -20,7 +20,10 @@ import Permission from 'Common/Types/Permission';
import ColumnAccessControl from 'Common/Types/Database/AccessControl/ColumnAccessControl';
import CurrentUserCanAccessRecordBy from 'Common/Types/Database/CurrentUserCanAccessRecordBy';
import SingularPluralName from 'Common/Types/Database/SingularPluralName';
import AllowAccessIfSubscriptionIsUnpaid from 'Common/Types/Database/AccessControl/AllowAccessIfSubscriptionIsUnpaid';
@AllowAccessIfSubscriptionIsUnpaid()
@TableAccessControl({
create: [Permission.Public],
read: [