652 lines
22 KiB
TypeScript
652 lines
22 KiB
TypeScript
import { Injectable } from '@angular/core';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { Observable, of, Subject, Subscription } from 'rxjs';
|
|
import { Price, InvoicePackage, Address, Invoice, SubscriptionPackage, StripeSubscription, PaymentMethod, UnpaidPackage, SubscriptionPaymentMethod, Charge, PaidAmount, AGNavSubscriptionShort, CustChargePkg, Usage, BillPeriod, UsagePackage, CheckoutPayment, Coupon, PMPkgEdit, PriceUsd, Acre, AGNavSubscription, Plan, Status, BillingInfoPackage, Package, Addon, TrialItem } from '@app/domain/models/subscription.model';
|
|
import { loadStripe, Stripe, StripeCardElement } from '@stripe/stripe-js';
|
|
import { DateUtils, UnitUtils, Utils } from '@app/shared/utils';
|
|
import { Mode, SUB, SubKeys, SubStripe, SubTexts, SubType, subPlans } from '@app/profile/common';
|
|
import { map, switchMap } from 'rxjs/operators';
|
|
import { IMembership } from '@app/auth/models/user.model';
|
|
import { Store } from '@ngrx/store';
|
|
import { getSubIntentMode } from '@app/reducers';
|
|
import { UserService } from './user.service';
|
|
|
|
export interface CCFormValues {
|
|
ccName: string,
|
|
card: StripeCardElement
|
|
}
|
|
const BASE_URL = '/subscription';
|
|
const MAX_PERCENT = 100;
|
|
const KEY = 'subIntent';
|
|
|
|
interface Option {
|
|
subscriptions?: { id: string, status: string }[];
|
|
coupon?: Coupon;
|
|
}
|
|
|
|
@Injectable({
|
|
providedIn: 'root'
|
|
})
|
|
export class SubscriptionService {
|
|
private _stripe: Stripe;
|
|
private _stripeLoadStatus$ = new Subject<boolean>();
|
|
|
|
get stripe(): Stripe {
|
|
return this._stripe;
|
|
}
|
|
|
|
set stripe(value: Stripe) {
|
|
if (!this._stripe) this._stripe = value;
|
|
}
|
|
|
|
private sub$: Subscription;
|
|
|
|
private subMode$ = this.store.select(getSubIntentMode);
|
|
private _subMode: Mode;
|
|
get subMode(): Mode {
|
|
return this._subMode;
|
|
}
|
|
|
|
constructor(
|
|
private readonly http: HttpClient,
|
|
private readonly store: Store<{}>,
|
|
private userSvc: UserService
|
|
) {
|
|
this.sub$ = this.subMode$.subscribe((mode) => this._subMode = mode);
|
|
}
|
|
|
|
// Rest endpoints
|
|
getPrices(): Observable<Price[]> {
|
|
return this.http.get<Price[]>(`${BASE_URL}/prices`);
|
|
}
|
|
|
|
getPaymentMethodList(custId: string): Observable<PaymentMethod[]> {
|
|
return this.http.get<PaymentMethod[]>(`${BASE_URL}/paymentMethods/${custId}`);
|
|
}
|
|
|
|
getDefPaymentMethods(custId: string): Observable<PaymentMethod> {
|
|
return this.http.get<PaymentMethod>(`${BASE_URL}/paymentMethods/${custId}/getDefault`);
|
|
}
|
|
|
|
getConfig(): Observable<{ config: string }> {
|
|
return this.http.get<{ config: string }>(`${BASE_URL}/config`);
|
|
}
|
|
|
|
getBillingAddress(applicatorId: string): Observable<Address> {
|
|
return this.http.get<Address>(`${BASE_URL}/billAddress/${applicatorId}`);
|
|
}
|
|
|
|
updateBillAddress(applicatorId: string, addrPkg: Address): Observable<Address> {
|
|
return this.http.put<Address>(`${BASE_URL}/billAddress/${applicatorId}`, addrPkg);
|
|
}
|
|
|
|
retrieveUpcomingInvoices(invoicePkg: InvoicePackage) {
|
|
return this.http.post<Invoice[]>(`${BASE_URL}/retrieveNextInvoices`, invoicePkg);
|
|
}
|
|
|
|
updateSubscription(subPkg: SubscriptionPackage): Observable<StripeSubscription[]> {
|
|
return this.http.post<StripeSubscription[]>(`${BASE_URL}/update`, subPkg);
|
|
}
|
|
|
|
fetchSubscriptions(custId: string): Observable<StripeSubscription[]> {
|
|
return this.http.get<StripeSubscription[]>(`${BASE_URL}?custId=${custId}&billInfo=true`);
|
|
}
|
|
|
|
fetchPayments(pmtPkg: { custId: string, byTime: string }): Observable<{ invoices: Invoice[], charges: Charge[] }> {
|
|
return this.http.post<{
|
|
invoices: Invoice[],
|
|
charges: Charge[]
|
|
}>(`${BASE_URL}/custInvoices`, {
|
|
custId: pmtPkg.custId,
|
|
byTime: pmtPkg.byTime
|
|
});
|
|
}
|
|
|
|
resumeUnpaidSub(unpaidSubs: string[]): Observable<Invoice[]> {
|
|
return this.http.post<Invoice[]>(`${BASE_URL}/resumeUnpaidSub`, {
|
|
unpaidSubs
|
|
});
|
|
}
|
|
|
|
payUnpaidSub(unpaidPkg: UnpaidPackage): Observable<Invoice[]> {
|
|
return this.http.post<Invoice[]>(`${BASE_URL}/payInvoice`, unpaidPkg);
|
|
}
|
|
|
|
updateSubsPaymentMethod(subPm: SubscriptionPaymentMethod): Observable<StripeSubscription[]> {
|
|
return this.http.post<StripeSubscription[]>(`${BASE_URL}/setSubsPaymentMethod`, subPm);
|
|
}
|
|
|
|
updateCustPaymentMethod(custId: string, pmId: string, setDefault?: boolean): Observable<PaymentMethod> {
|
|
return this.http.put<PaymentMethod>(`${BASE_URL}/paymentMethods/${custId}`, {
|
|
pmId,
|
|
setDefault
|
|
});
|
|
}
|
|
|
|
getCustCharges(chargePkg: CustChargePkg): Observable<any[]> {
|
|
return this.http.post<any[]>(`${BASE_URL}/custCharges`, chargePkg);
|
|
}
|
|
|
|
retrieveUsage(usgPkg: UsagePackage): Observable<Usage> {
|
|
return this.http.post<Usage>(`${BASE_URL}/custUsages`, usgPkg);
|
|
}
|
|
|
|
retrieveCurrUsage(custId: string, byPuid: string): Observable<Usage> {
|
|
return this.retrieveBilPeriod(custId).pipe(
|
|
switchMap((_billPeriods) => {
|
|
const curPeriod = _billPeriods.sort((p1, p2) => p1.periodEnd - p2.periodEnd).reverse()[0];
|
|
return this.retrieveUsage({
|
|
byPuid: byPuid,
|
|
fromTS: DateUtils.startUtcTS(curPeriod?.periodStart),
|
|
toTS: DateUtils.endUtcTS(curPeriod?.periodEnd)
|
|
});
|
|
}),
|
|
map((usage) => usage)
|
|
)
|
|
}
|
|
|
|
retrieveBilPeriod(custId: string, subTypes?: string[]): Observable<BillPeriod[]> {
|
|
return this.http.post<BillPeriod[]>(`${BASE_URL}/subBillPeriods`, {
|
|
custId,
|
|
subTypes
|
|
});
|
|
}
|
|
|
|
editSub(subsSettings: { subId: string, cancelAtPeriodEnd: boolean }[]): Observable<StripeSubscription[]> {
|
|
return this.http.post<StripeSubscription[]>(`${BASE_URL}/setSubsSettings`, {
|
|
subsSettings
|
|
});
|
|
}
|
|
|
|
getCoupon(coupon: string): Observable<Coupon> {
|
|
return this.http.get<Coupon>(`${BASE_URL}/getCoupon/${coupon}`);
|
|
}
|
|
|
|
editPM(custId: string, pkg: PMPkgEdit): Observable<PaymentMethod> {
|
|
return this.http.put<PaymentMethod>(`${BASE_URL}/paymentMethods/${custId}`, pkg);
|
|
}
|
|
|
|
addPM(custId: string, pmId: string, setDefault?: boolean): Observable<PaymentMethod> {
|
|
return this.http.post<PaymentMethod>(`${BASE_URL}/paymentMethods/${custId}`, {
|
|
pmId,
|
|
setDefault
|
|
});
|
|
}
|
|
|
|
deletePM(custId: string, pmId: string): Observable<PaymentMethod> {
|
|
return this.http.request<PaymentMethod>('delete', `${BASE_URL}/paymentMethods/${custId}`, {
|
|
body: {
|
|
pmId
|
|
}
|
|
});
|
|
}
|
|
|
|
// Utils
|
|
hasSubsWithStatus(subs: StripeSubscription[], status: string): boolean {
|
|
return subs?.some((sub) => sub?.status === `${status}`);
|
|
}
|
|
|
|
isRequirePaymentMethod(subs: StripeSubscription[]): boolean {
|
|
return subs?.some((sub) => sub?.latest_invoice?.payment_intent?.status === SubStripe.REQUIRE_PAYMENT_METHOD);
|
|
}
|
|
|
|
isRequireAction(subs: StripeSubscription[]): boolean {
|
|
return subs?.some((sub) => sub?.latest_invoice?.payment_intent?.status === SubStripe.REQUIRE_ACTION);
|
|
}
|
|
|
|
getReqPmSubscription(subs: StripeSubscription[]): StripeSubscription {
|
|
return subs?.find((sub) => sub?.latest_invoice?.payment_intent?.status === SubStripe.REQUIRE_PAYMENT_METHOD);
|
|
}
|
|
|
|
getReqActionSubscription(subs: StripeSubscription[]): StripeSubscription {
|
|
SubStripe.REQUIRE_ACTION
|
|
return subs?.find((sub) => sub?.latest_invoice?.payment_intent?.status === SubStripe.REQUIRE_ACTION);
|
|
}
|
|
|
|
atCheckoutReviewStage(): boolean {
|
|
const subIntent = JSON.parse(sessionStorage.getItem(KEY));
|
|
return subIntent?.stage === SUB.CHKOUT_REV;
|
|
}
|
|
|
|
atStage(stage: string): boolean {
|
|
const subIntent = JSON.parse(sessionStorage.getItem(KEY));
|
|
return subIntent?.stage === `${stage}`;
|
|
}
|
|
|
|
checkSubStatus(subs: AGNavSubscriptionShort[], status: string, op: string) {
|
|
const binaryOp = (op: string) => new Function('x', 'y', `return x ${op} y;`);
|
|
return subs?.some((sub) => binaryOp(op)(sub?.status, status));
|
|
}
|
|
|
|
getPeriod(periodEnd: number): number {
|
|
return periodEnd - DateUtils.startUtcTS(DateUtils.currUTC());
|
|
}
|
|
|
|
dayUsedPerct(periodStart: number, periodEnd: number) {
|
|
const period = periodEnd - periodStart;
|
|
const dayUsed = DateUtils.currUTC() - periodStart;
|
|
const curPeriod = this.getPeriod(periodEnd);
|
|
if (period === 0) return 0;
|
|
if (dayUsed <= 0) return 0;
|
|
if (curPeriod <= 0) return 100;
|
|
return Math.floor((dayUsed / period) * MAX_PERCENT);
|
|
}
|
|
|
|
curDayRemain(periodEnd: number) {
|
|
const SECS_PER_DAY = 86400;
|
|
const curPeriod = this.getPeriod(periodEnd);
|
|
if (curPeriod <= 0) return 0;
|
|
return Math.floor(curPeriod / SECS_PER_DAY);
|
|
}
|
|
|
|
acrUsedPerct(acrUsed: number, maxAcr: number) {
|
|
if (!+maxAcr || !+acrUsed || maxAcr === 0) return 0;
|
|
if (acrUsed >= maxAcr) return 100;
|
|
return Math.floor((acrUsed / maxAcr) * MAX_PERCENT);
|
|
}
|
|
|
|
private calcTotalAmount(lines): number {
|
|
if (Utils.isEmptyArray(lines)) return 0;
|
|
return lines?.map((line) => line?.amount).reduce((t1, t2) => t1 + t2, 0);
|
|
}
|
|
|
|
private extractLineTax(lines): [] {
|
|
if (Utils.isEmptyArray(lines)) return [];
|
|
return lines?.map((line) => line?.tax_amounts).flat();
|
|
}
|
|
|
|
private calcInvoice(invoices: Invoice[], coupon?: Coupon): CheckoutPayment {
|
|
let lines = [];
|
|
invoices?.map((inv) => lines = lines.concat(inv?.lines?.data));
|
|
const pmtTotalTax = this.calcTotalAmount(this.extractLineTax(lines));
|
|
const pmt = {
|
|
payment: {
|
|
lineItems: lines,
|
|
totalAmount: this.calcTotalAmount(lines) + pmtTotalTax,
|
|
totalTax: pmtTotalTax
|
|
}
|
|
};
|
|
if (coupon) {
|
|
return this.applyCoupon(pmt, coupon);
|
|
}
|
|
return pmt;
|
|
}
|
|
|
|
private calcInvoiceWithProrate(invoices: Invoice[], coupon?: Coupon): CheckoutPayment {
|
|
let lines = [];
|
|
invoices.map((inv) => lines = lines.concat(inv?.lines?.data?.filter((line) => line?.period?.start === inv?.subscription_proration_date)));
|
|
const pmtLines = lines.filter((line) => line.amount >= 0);
|
|
const refLines = lines.filter((line) => line.amount < 0);
|
|
let pmt: CheckoutPayment;
|
|
const pmtTotalTax = this.calcTotalAmount(this.extractLineTax(pmtLines));
|
|
if (refLines.length > 0) {
|
|
const refTotalTax = this.calcTotalAmount(this.extractLineTax(refLines));
|
|
pmt = {
|
|
payment: {
|
|
lineItems: pmtLines,
|
|
totalAmount: this.calcTotalAmount(pmtLines) + pmtTotalTax,
|
|
totalTax: pmtTotalTax
|
|
},
|
|
refund: {
|
|
lineItems: refLines,
|
|
totalAmount: this.calcTotalAmount(refLines) + refTotalTax,
|
|
totalTax: refTotalTax
|
|
}
|
|
};
|
|
} else {
|
|
pmt = {
|
|
payment: {
|
|
lineItems: pmtLines,
|
|
totalAmount: this.calcTotalAmount(pmtLines) + pmtTotalTax,
|
|
totalTax: pmtTotalTax
|
|
}
|
|
};
|
|
}
|
|
|
|
if (coupon) {
|
|
return this.applyCoupon(pmt, coupon);
|
|
}
|
|
return pmt;
|
|
}
|
|
|
|
calcChkoutPayment(invoices: Invoice[], opt?: Option): CheckoutPayment {
|
|
if (Utils.isEmptyArray(invoices)) return { payment: { totalAmount: 0, totalTax: 0, lineItems: [] } };
|
|
const prorateInvs = invoices.filter((inv) => inv?.lines?.data?.some((line) => line?.period?.start === inv?.subscription_proration_date));
|
|
const hasNoProrate = prorateInvs.length === 0;
|
|
const hasUnResolvedInvoice = opt?.subscriptions?.some((sub) =>
|
|
sub.status === SubStripe.UNPAID ||
|
|
sub.status === SubStripe.INCOMPLETE ||
|
|
sub.status === SubStripe.PAST_DUE ||
|
|
sub.status === SubStripe.OVERDUE
|
|
) || hasNoProrate;
|
|
if (hasUnResolvedInvoice) {
|
|
return this.calcInvoice(invoices, opt?.coupon);
|
|
}
|
|
return this.calcInvoiceWithProrate(invoices, opt?.coupon);
|
|
}
|
|
|
|
calcAmount(invoices: Invoice[], opt?: Option): PaidAmount {
|
|
if (Utils.isEmptyArray(invoices)) return { totalExcludingTax: 0, totalTax: 0, total: 0 };
|
|
const pmt = this.calcChkoutPayment(invoices, opt);
|
|
return {
|
|
totalExcludingTax: pmt.payment.totalAmount - pmt.payment.totalTax,
|
|
totalTax: pmt.payment.totalTax,
|
|
total: pmt.payment.totalAmount, discount: pmt.payment.discount
|
|
};
|
|
}
|
|
|
|
hasInValTaxLoc(subs: StripeSubscription[]): boolean {
|
|
if (Utils.isEmptyArray(subs)) return false;
|
|
return subs?.some((sub) => sub?.latest_invoice?.automatic_tax?.enabled
|
|
&& sub?.latest_invoice?.automatic_tax?.status === SubStripe.REQ_LOC_INPUT);
|
|
}
|
|
|
|
private applyCoupon(pmt: CheckoutPayment, coupon: Coupon): CheckoutPayment {
|
|
let clonedPmt: CheckoutPayment = { ...pmt };
|
|
if (coupon) {
|
|
if (coupon.percent_off) {
|
|
const amountOff = (clonedPmt.payment.totalAmount - clonedPmt.payment.totalTax) * (coupon.percent_off / 100);
|
|
clonedPmt.payment = {
|
|
...clonedPmt.payment,
|
|
totalAmount: clonedPmt.payment.totalAmount - amountOff,
|
|
totalTax: clonedPmt.payment.totalTax,
|
|
discount: { amountOff, percentOff: coupon.percent_off }
|
|
};
|
|
return clonedPmt;
|
|
}
|
|
let totalAmount = clonedPmt.payment.totalAmount - coupon.amount_off;
|
|
clonedPmt.payment = {
|
|
...clonedPmt.payment,
|
|
totalAmount: totalAmount >= 0 ? totalAmount : 0,
|
|
totalTax: clonedPmt.payment.totalTax,
|
|
discount: { amountOff: coupon.amount_off }
|
|
};
|
|
return clonedPmt;
|
|
}
|
|
return clonedPmt;
|
|
}
|
|
|
|
getInvCoupon(invoices: Invoice[]): Coupon {
|
|
if (Utils.isEmptyArray(invoices)) return;
|
|
return invoices?.[0]?.discount?.coupon;
|
|
}
|
|
|
|
crtCardDesc(brand: string, last4: string): string {
|
|
if (!brand && !last4) return '';
|
|
return `${brand.charAt(0).toUpperCase()}${brand.slice(1)} ${SubTexts.ending} **** ${last4}`;
|
|
}
|
|
|
|
crtExp(expMonth, expYear): string {
|
|
if (!expMonth && !expYear) return '';
|
|
return expMonth.toString().length === 1
|
|
? `0${expMonth}/${expYear}`
|
|
: `${expMonth}/${expYear}`;
|
|
}
|
|
|
|
formatCurrency(currency: PriceUsd): string {
|
|
const DEFAULT_CURRENCY = '$0';
|
|
if (currency) {
|
|
const priceToUS = (price: PriceUsd): string => {
|
|
if (price) {
|
|
return new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' }).format(+price / 100);
|
|
}
|
|
return DEFAULT_CURRENCY;
|
|
}
|
|
const formatNegative = (currency: number): string => {
|
|
const usPrice = priceToUS(Math.abs(currency));
|
|
return `$(${usPrice.substring(1, usPrice.length)})`;
|
|
}
|
|
return +currency >= 0 ? priceToUS(currency) : formatNegative(+currency);
|
|
}
|
|
return DEFAULT_CURRENCY;
|
|
}
|
|
|
|
convMaxAcre(maxAcres: number | string): string {
|
|
if (!maxAcres) return '';
|
|
const THOUSAND = 1000;
|
|
const maxAcrToK = +maxAcres / THOUSAND;
|
|
return maxAcrToK > 0 ? `${maxAcrToK}K` : maxAcres.toString();
|
|
}
|
|
|
|
hasOpenSub(subs: AGNavSubscription[] | StripeSubscription[]): boolean {
|
|
if (Utils.isEmptyArray(subs)) return false;
|
|
return subs?.some((sub) =>
|
|
sub.status === SubStripe.INCOMPLETE ||
|
|
sub.status === SubStripe.PAST_DUE ||
|
|
sub.status === SubStripe.OVERDUE ||
|
|
sub.status === SubStripe.UNPAID
|
|
);
|
|
}
|
|
|
|
updateMembShip(subscriptions: StripeSubscription[], membership: IMembership): IMembership {
|
|
if (Utils.isEmptyArray(subscriptions)) return membership;
|
|
return {
|
|
...membership,
|
|
endOfPeriod: subscriptions?.find((sub) => sub.metadata.type === SubType.PACKAGE)?.latest_invoice.period_end ||
|
|
subscriptions?.find((sub) => sub.metadata.type === SubType.ADDON)?.latest_invoice.period_end,
|
|
subscriptions: subscriptions?.map((sub) => ({
|
|
id: sub.id,
|
|
periodEnd: sub.current_period_end,
|
|
periodStart: sub.current_period_start,
|
|
status: sub.status,
|
|
items: sub.items.data?.map((item) => ({
|
|
price: item.price.lookup_key,
|
|
quantity: item.quantity
|
|
})),
|
|
type: sub.metadata.type,
|
|
cancelAtPeriodEnd: sub.cancel_at_period_end
|
|
}))
|
|
};
|
|
}
|
|
|
|
createSubPlan(subscriptions: StripeSubscription[], membership: IMembership, usage: Usage): Plan {
|
|
if (Utils.isEmptyArray(membership?.subscriptions)
|
|
|| Utils.isEmptyArray(subscriptions)
|
|
|| this.hasOpenSub(subscriptions)) {
|
|
return { subscriptions, membership, package: {}, addon: {} };
|
|
}
|
|
|
|
const getSubscriptionItem = (type: SubType) =>
|
|
membership.subscriptions.find(sub => sub.type === type
|
|
&& (sub.status === SubStripe.ACTIVE || sub.status === SubStripe.TRIALING))?.items[0];
|
|
|
|
const createAcrePlan = (currUsage: number, limit: number): Acre => ({
|
|
currUsage,
|
|
limit,
|
|
overLimit: currUsage > (limit ? limit : Infinity)
|
|
});
|
|
|
|
const pkg = getSubscriptionItem(SubType.PACKAGE);
|
|
const addon = getSubscriptionItem(SubType.ADDON);
|
|
const pkgPrice = pkg?.price;
|
|
|
|
const maxAcres = isNaN(+pkg?.metadata?.maxAcres) ? 0 : +pkg?.metadata?.maxAcres;
|
|
const acre = createAcrePlan(UnitUtils.haToArea(usage.ttArea, true), maxAcres || subPlans[pkgPrice]?.maxAcres);
|
|
const maxVehicles = isNaN(+pkg?.metadata?.maxVehicles) ? 0 : +pkg?.metadata?.maxVehicles;
|
|
const pkgNumVeh = maxVehicles || subPlans[pkgPrice]?.maxVehicles || 0;
|
|
const trackNumVeh = addon?.quantity || 0;
|
|
|
|
const packagePlan = pkg ? {
|
|
[pkgPrice]: {
|
|
acre,
|
|
airCraft: {
|
|
numOfVehicle: pkgNumVeh
|
|
}
|
|
}
|
|
} : {};
|
|
|
|
const addonPlan = addon ? {
|
|
[SubKeys.TRACKING]: {
|
|
airCraft: {
|
|
numOfVehicle: trackNumVeh
|
|
},
|
|
acre
|
|
}
|
|
} : {};
|
|
|
|
return { subscriptions, membership, package: packagePlan, addon: addonPlan };
|
|
}
|
|
|
|
isStatusMatchingCode(status: Status, code: string) {
|
|
return status?.code === code;
|
|
}
|
|
|
|
isUnderReview(status: Status) {
|
|
return this.isStatusMatchingCode(status, SUB.AC_REVIEW);
|
|
}
|
|
|
|
fmtSubMsg(text: string, key: PriceUsd, vehicle: { trkQuantity?: number, pkgQuantity?: number }): string {
|
|
return text?.replace('#pkg#', subPlans[key].name)
|
|
.replace('#quantity#', `${vehicle.trkQuantity}` || '')
|
|
.replace('#maxAC#', `${vehicle.pkgQuantity}` || '') || '';
|
|
}
|
|
|
|
toVehRange(precedingMax: number, maxVehicles: number): string {
|
|
const MIN = 1;
|
|
const MAX = 10;
|
|
const lowerRange = precedingMax + MIN;
|
|
return maxVehicles > MIN && maxVehicles <= MAX
|
|
? lowerRange ? `${lowerRange}-${maxVehicles}`
|
|
: `${maxVehicles}`
|
|
: `${maxVehicles}`;
|
|
}
|
|
|
|
convertAddr(address) {
|
|
address.postal_code = address?.postalCode;
|
|
delete address?.postalCode;
|
|
delete address?.name;
|
|
delete address?.valid;
|
|
return address;
|
|
}
|
|
|
|
createBillingInfoPackage(applicatorId): Observable<BillingInfoPackage> {
|
|
let billingInfoPackage: BillingInfoPackage;
|
|
return this.getBillingAddress(applicatorId).pipe(
|
|
switchMap((address: Address) => {
|
|
const hasExistingAdr = address && Object.keys(address)?.some((key) =>
|
|
key === 'name' ||
|
|
key === 'postalCode' ||
|
|
key === 'line1'
|
|
);
|
|
if (hasExistingAdr) {
|
|
return of(billingInfoPackage = {
|
|
billingInfo: {
|
|
applicatorId,
|
|
name: address.name,
|
|
address: this.convertAddr(address)
|
|
}
|
|
});
|
|
} else {
|
|
return this.userSvc.getUser(applicatorId).pipe(
|
|
map((user) => {
|
|
return billingInfoPackage = {
|
|
isNewAccount: true,
|
|
billingInfo: {
|
|
applicatorId,
|
|
name: user.name,
|
|
address: {
|
|
line1: user.address,
|
|
country: user.country,
|
|
city: '',
|
|
state: '',
|
|
postal_code: ''
|
|
}
|
|
}
|
|
}
|
|
})
|
|
);
|
|
}
|
|
}),
|
|
);
|
|
}
|
|
|
|
createTrialItems(selPkg: Package, selAddons: Addon[]): TrialItem[] {
|
|
const trialItems = selAddons?.map((addon) => ({
|
|
description: addon.desc,
|
|
amount: +addon.price * addon.quantity,
|
|
quantity: addon.quantity,
|
|
price: {
|
|
lookup_key: addon.lookupKey,
|
|
unit_amount: +addon.price
|
|
}
|
|
})) || [];
|
|
|
|
if (selPkg?.lookupKey) {
|
|
trialItems.unshift({
|
|
description: selPkg.desc,
|
|
amount: +selPkg.price,
|
|
quantity: 1,
|
|
price: {
|
|
lookup_key: selPkg.lookupKey,
|
|
unit_amount: +selPkg.price
|
|
}
|
|
});
|
|
}
|
|
|
|
return trialItems;
|
|
}
|
|
|
|
getDateOptions(): { label: string, value: string }[] {
|
|
const options = [
|
|
{ label: $localize`:@@1m:past 1 month`, value: '1m' },
|
|
{ label: $localize`:@@3m:past 3 months`, value: '3m' },
|
|
{ label: $localize`:@@6m:past 6 months`, value: '6m' },
|
|
];
|
|
const currYear = new Date().getUTCFullYear();
|
|
for (let i = currYear; i > currYear - 3; i--) {
|
|
options.push({ label: i.toString(), value: i.toString() });
|
|
}
|
|
return options;
|
|
}
|
|
|
|
get stripeLoadStatus$() {
|
|
return this._stripeLoadStatus$;
|
|
}
|
|
|
|
async loadStripeApi(pk: string) {
|
|
try {
|
|
this.stripe = await loadStripe(pk);
|
|
} catch (err) {
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
loadStripePromise(): Promise<void> {
|
|
if (this.stripe) {
|
|
return Promise.resolve(); // Stripe is already loaded, no need to load again
|
|
}
|
|
|
|
return this.getConfig().pipe(
|
|
switchMap((res) => this.loadStripeApi(res.config))
|
|
).toPromise().then(() => {
|
|
this.stripeLoadStatus$.next(true);
|
|
}).catch((err) => {
|
|
console.error('Failed to load Stripe API:', err);
|
|
this.stripeLoadStatus$.error(false);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Updates the billing address sequence for a user and returns both the updated address and user.
|
|
* @param userId The user's id
|
|
* @param address The address to update
|
|
* @returns Observable<{ address: Address, user: User }>
|
|
*/
|
|
public updateBillingAddressSequence(userId: string, address: Address) {
|
|
const { isBilling, ...addressWithoutBilling } = address; // Remove isBilling property if it exists
|
|
return this.updateBillAddress(userId, addressWithoutBilling).pipe(
|
|
switchMap((updatedAddress: Address) =>
|
|
this.userSvc.getUser(userId, { withAddresses: true }).pipe(
|
|
switchMap((user) => {
|
|
return of({ address: updatedAddress, user })
|
|
})
|
|
)
|
|
)
|
|
);
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
if (this.sub$) this.sub$.unsubscribe();
|
|
}
|
|
}
|