Skip to content
Open
18 changes: 15 additions & 3 deletions app/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
import { Module } from "@nestjs/common";
import { RedisModule } from "@recursyve/nestjs-redis";
import {
QuickBooksAttachablesModule,
QuickBooksCustomersModule,
QuickBooksEstimatesModule,
QuickBooksInvoicesModule,
QuickBooksModule,
QuickBooksScopes
QuickBooksPaymentsModule,
QuickBooksScopes,
QuickBooksStore
} from "../../lib";
import { CustomersController } from "./customers/customers.controller";
import { InvoicesController } from "./invoices/invoices.controller";
import { RedisQuickbooksStore } from "./store/redis.quickbooks.store";

@Module({
imports: [
QuickBooksModule.forRoot({
imports: [RedisModule],
config: {
mode: "sandbox",
serverUri: "http://localhost:3000",
serverUri: "https://06dc-72-10-138-232.ngrok.io",
scopes: [QuickBooksScopes.Accounting],
redirection: {
successUrl: "http://localhost:3000/customer",
errorUrl: "http://localhost:3000/customer"
}
},
store: {
provide: QuickBooksStore,
useClass: RedisQuickbooksStore
}
}),
QuickBooksAttachablesModule,
QuickBooksCustomersModule,
QuickBooksInvoicesModule
QuickBooksEstimatesModule,
QuickBooksInvoicesModule,
QuickBooksPaymentsModule
],
controllers: [CustomersController, InvoicesController]
})
Expand Down
38 changes: 26 additions & 12 deletions app/src/customers/customers.controller.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Controller, Get, Post } from "@nestjs/common";
import { Body, Controller, Get, Post } from "@nestjs/common";
import { lastValueFrom } from "rxjs";
import { Op, QuickBooksCustomersService } from "../../../lib";

@Controller("customer")
Expand All @@ -8,22 +9,35 @@ export class CustomersController {

@Get()
public async getAll() {
return (await this.customersService.withDefaultCompany()).query({
/* MetaData: {
LastUpdatedTime: {
[Op.gt]: "2015-03-01"
}
}*/
}).toPromise().then(x => x.QueryResponse.Customer);
const service = await this.customersService.withDefaultCompany();
const count = await lastValueFrom(service.count({}));
return lastValueFrom(service.query({
/* MetaData: {
LastUpdatedTime: {
[Op.gt]: "2015-03-01"
}
}*/
}, { maxResult: count?.QueryResponse?.totalCount })).then(x => x.QueryResponse.Customer);
}

@Get("name")
public async GetByName() {
const service = await this.customersService.withDefaultCompany();
const response = await lastValueFrom(service.query({
DisplayName: {
[Op.contains]: "'"
}
}));
return response.QueryResponse;
}

@Post()
public async create() {
return (await this.customersService.withDefaultCompany()).create({
DisplayName: "My new customer",
public async create(@Body() dto: any) {
return lastValueFrom((await this.customersService.withDefaultCompany()).create({
DisplayName: dto.name,
PrimaryEmailAddr: {
Address: "julien@recursyve.io"
}
}).toPromise();
}));
}
}
43 changes: 40 additions & 3 deletions app/src/invoices/invoices.controller.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import { Controller, Get, Header, Param, Put, Res } from "@nestjs/common";
import { Controller, Get, Header, Param, Patch, Put, Res } from "@nestjs/common";
import * as fs from "fs/promises";
import * as path from "path";
import { lastValueFrom } from "rxjs";
import {
Op,
QuickBooksAttachablesService,
QuickBooksAttachablesUploadResponseModel,
QuickBooksInvoicesService
QuickBooksAttachablesUploadResponseModel, QuickBooksEstimatesService,
QuickBooksInvoicesService, QuickBooksPaymentsService
} from "../../../lib";

@Controller("invoice")
export class InvoicesController {
constructor(
private readonly attachablesService: QuickBooksAttachablesService,
private readonly estimatesService: QuickBooksEstimatesService,
private readonly invoicesService: QuickBooksInvoicesService,
private readonly paymentsService: QuickBooksPaymentsService,
) {}

@Get()
Expand Down Expand Up @@ -52,4 +54,39 @@ export class InvoicesController {
]
}));
}

@Patch(":id/customer")
public async updateCustomer(@Param("id") id: string): Promise<void> {
const estimateService = await this.estimatesService.withDefaultCompany();
const estimate = await estimateService.readById("213").toPromise();
await estimateService.sparseUpdate(estimate.Estimate, {
CustomerRef: {
value: "70",
}
}).toPromise();

const paymentService = await this.paymentsService.withDefaultCompany();
const payment = await paymentService.readById("215").toPromise();
const p = await paymentService.fullUpdate(payment.Payment, {
...payment.Payment,
CustomerRef: {
value: "70",
},
Line: []
}).toPromise();

const service = await this.invoicesService.withDefaultCompany();
const invoice = await service.readById(id).toPromise();
await service.sparseUpdate(invoice.Invoice, {
CustomerRef: {
value: "70"
},
LinkedTxn: [
{
TxnType: "Payment",
TxnId: p.Payment.Id
} as any
]
}).toPromise();
}
}
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ export * from "./modules/store";
export * from "./modules/tax-codes";
export * from "./modules/vendors";
export * from "./modules/webhooks";
export * from "./modules/terms";
28 changes: 21 additions & 7 deletions lib/modules/common/base.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { HttpService } from "@nestjs/axios";
import { HttpStatus, Injectable } from "@nestjs/common";
import { AxiosError } from "axios";
import { catchError, Observable, of, throwError } from "rxjs";
import { catchError, Observable, throwError } from "rxjs";
import { map, mergeMap } from "rxjs/operators";
import { QueryUtils } from "../../utils/query.utils";
import { QuickBooksAuthService } from "../auth/services/auth.service";
import { QuickbooksUnauthorizedException, QuickbooksBadRequestException } from "./exceptions";
import { WhereOptions } from "./models";
import { QuickbooksBadRequestException, QuickbooksUnauthorizedException } from "./exceptions";
import { QueryOptions, QueryStatementType, WhereOptions } from "./models";

@Injectable()
export class BaseService<Response, Query, QueryResponse> {
Expand All @@ -30,9 +30,23 @@ export class BaseService<Response, Query, QueryResponse> {
return this.authService.mode === "production" ? this.liveUrl : this.sandboxUrl;
}

public query(condition: WhereOptions<Query>): Observable<QueryResponse> {
public query(condition: WhereOptions<Query>, options?: QueryOptions): Observable<QueryResponse> {
return this.getHttpHeaders().pipe(
mergeMap((headers) => this.http.get<QueryResponse>(this.queryUrl(condition), {
mergeMap((headers) => this.http.get<QueryResponse>(this.queryUrl(QueryStatementType.Select, condition, options), {
headers,
params: {
...this.minorVersion
}
}))
).pipe(
map(x => x.data),
catchError((e) => throwError(() => this.catchError(e)))
);
}

public count(condition: WhereOptions<Query>): Observable<QueryResponse> {
return this.getHttpHeaders().pipe(
mergeMap((headers) => this.http.get<QueryResponse>(this.queryUrl(QueryStatementType.Count, condition), {
headers,
params: {
...this.minorVersion
Expand Down Expand Up @@ -80,8 +94,8 @@ export class BaseService<Response, Query, QueryResponse> {
);
}

protected queryUrl(condition: WhereOptions<any>): string {
return `${this.apiUrl}/v3/company/${this.realm}/query?${QueryUtils.generateQuery(this.resource, condition)}`;
protected queryUrl(statement: QueryStatementType, condition: WhereOptions<any>, options?: QueryOptions): string {
return `${this.apiUrl}/v3/company/${this.realm}/query?${QueryUtils.generateQuery(this.resource, statement, condition, options)}`;
}

protected url(path: string): string {
Expand Down
1 change: 1 addition & 0 deletions lib/modules/common/exceptions/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./quickbooks.exception";
export * from "./quickbooks-unauthorized.exception";
export * from "./quickbooks-bad-request.exception";
10 changes: 10 additions & 0 deletions lib/modules/common/models/query.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,3 +79,13 @@ export type WhereAttributeHash<T> = {
};

export type WhereOptions<T> = WhereAttributeHash<T> | AndOperator<T>;

export interface QueryOptions {
startPosition?: number;
maxResult?: number;
}

export enum QueryStatementType {
Select,
Count
}
1 change: 1 addition & 0 deletions lib/modules/common/models/response.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ export interface QuickBooksDeleteResponseModel {
export interface QuickBooksQueryResponseModel {
startPosition: number;
maxResults: number;
totalCount: number;
}
2 changes: 1 addition & 1 deletion lib/modules/items/services/items.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export class QuickBooksCompanyItemsService extends BaseService<
}

public fullUpdate(id: string, token: string, dto: FullUpdateQuickBooksItemsDto): Observable<QuickBooksItemsResponseModel>;
public fullUpdate(customer: QuickBooksItems, dto: FullUpdateQuickBooksItemsDto): Observable<QuickBooksItemsResponseModel>;
public fullUpdate(item: QuickBooksItems, dto: FullUpdateQuickBooksItemsDto): Observable<QuickBooksItemsResponseModel>;
public fullUpdate(
...args: [string | QuickBooksItems, string | FullUpdateQuickBooksItemsDto, FullUpdateQuickBooksItemsDto?]
): Observable<QuickBooksItemsResponseModel> {
Expand Down
4 changes: 2 additions & 2 deletions lib/modules/store/store.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { TokensModel } from "../auth/models/tokens.model";
@Injectable()
export abstract class QuickBooksStore {
public abstract registerCompany(realm: string): Promise<void>;
public abstract getDefaultCompany(): Promise<string>;
public abstract getDefaultCompany(): Promise<string | null>;
public abstract setToken(realm: string, token: TokensModel): Promise<void>;
public abstract getToken(realm: string): Promise<TokensModel>;
public abstract getToken(realm: string): Promise<TokensModel | null>;
}
7 changes: 7 additions & 0 deletions lib/modules/terms/dto/terms.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface CreateQuickBooksTermsDto {
Name: string;
DiscountPercent: number;
Active: boolean;
DueDays: number;
DayOfMonthDue: number;
}
4 changes: 4 additions & 0 deletions lib/modules/terms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from "./dto/terms.dto";
export * from "./models/terms.model";
export * from "./services/terms.service";
export * from "./terms.module";
6 changes: 6 additions & 0 deletions lib/modules/terms/models/terms-query.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { QuickBooksQueryModel } from "../../common";

export interface QuickBooksTermsQuery extends QuickBooksQueryModel {
Name: string;
Active: boolean;
}
10 changes: 10 additions & 0 deletions lib/modules/terms/models/terms-response.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { QuickBooksQueryResponseModel, QuickBooksResponseModel } from "../../common";
import { QuickBooksTerms } from "./terms.model";

export interface QuickBooksTermsResponseModel extends QuickBooksResponseModel {
Term: QuickBooksTerms;
}

export interface QuickBooksTermsQueryResponseModel extends QuickBooksResponseModel {
QueryResponse: QuickBooksQueryResponseModel & { Term: QuickBooksTerms[]; };
}
12 changes: 12 additions & 0 deletions lib/modules/terms/models/terms.model.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { QuickBooksModel } from "../../common";

export interface QuickBooksTerms extends QuickBooksModel {
Name: string;
Active: boolean;
DueDays: number;
DueNextMonthDays: number;
DayOfMonthDue: number;
DiscountDayOfMonth: number;
DiscountPercent: number;
DiscountDays: number;
}
42 changes: 42 additions & 0 deletions lib/modules/terms/services/terms.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Injectable } from "@nestjs/common";
import { Observable } from "rxjs";
import { QuickBooksAuthService } from "../../auth/services/auth.service";
import { BaseService } from "../../common/base.service";
import { QuickBooksStore } from "../../store";
import { QuickBooksTermsQueryResponseModel, QuickBooksTermsResponseModel } from "../models/terms-response.model";
import { QuickBooksTermsQuery } from "../models/terms-query.model";
import { HttpService } from "@nestjs/axios";
import { QuickBooksTerms } from "../models/terms.model";
import { CreateQuickBooksTermsDto } from "../dto/terms.dto";

@Injectable()
export class QuickBooksTermsService {
constructor(
private readonly authService: QuickBooksAuthService,
private readonly http: HttpService,
private readonly store: QuickBooksStore
) {
}

public async withDefaultCompany(): Promise<QuickBooksCompanyTermsService> {
return this.forCompany(await this.store.getDefaultCompany());
}

public forCompany(realm: string): QuickBooksCompanyTermsService {
return new QuickBooksCompanyTermsService(realm, this.authService, this.http);
}
}

export class QuickBooksCompanyTermsService extends BaseService<QuickBooksTerms, QuickBooksTermsQuery, QuickBooksTermsQueryResponseModel> {
constructor(realm: string, authService: QuickBooksAuthService, http: HttpService) {
super(realm, "term", authService, http);
}

public create(dto: CreateQuickBooksTermsDto): Observable<QuickBooksTermsResponseModel> {
return this.post(dto);
}

public readById(id: string): Observable<QuickBooksTermsResponseModel> {
return this.get(id);
}
}
11 changes: 11 additions & 0 deletions lib/modules/terms/terms.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from "@nestjs/common";
import { QuickBooksAuthModule } from "../auth/auth.module";
import { QuickBooksTermsService } from "./services/terms.service";
import { HttpModule } from "@nestjs/axios";

@Module({
imports: [QuickBooksAuthModule, HttpModule],
providers: [QuickBooksTermsService],
exports: [QuickBooksTermsService]
})
export class QuickBooksTermsModule {}
14 changes: 9 additions & 5 deletions lib/utils/operators.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,11 @@ export class OperatorsUtils {
case Op.notLike:
return `${attribute} not like ${this.transformValue(value)}`;
case Op.beginsWith:
return `${attribute} like '${value}%'`;
return `${attribute} like '${this.escapeString(value)}%'`;
case Op.endsWith:
return `${attribute} like '%${value}'`;
return `${attribute} like '%${this.escapeString(value)}'`;
case Op.contains:
return `${attribute} like '%${value}%'`;
return `${attribute} like '%${this.escapeString(value)}%'`;
}
}

Expand All @@ -40,12 +40,16 @@ export class OperatorsUtils {
return value.toISOString();
}
if (typeof value === "string") {
return `'${value}'`;
return `'${this.escapeString(value)}'`;
}
if (Array.isArray(value)) {
return value.map(x => typeof x === "string" ? `'${x}'` : x).join(",");
return value.map(x => typeof x === "string" ? `'${this.escapeString(x)}'` : x).join(",");
}

return value;
}

public static escapeString(value: string): string {
return value.replace(/'/g, "\\'");
}
}
Loading