import { Injectable } from '@nestjs/common' import { get, isEmpty, isNil, last, sortBy } from 'lodash' import { HttpService } from '../http' import { ExternalExceptionError, NotFoundError } from '../../errors' import { Subscription } from '../../models/subscription.model' import { AppleInAppSubscription, ReceiptInfo } from './types' const SANDBOX_STATUS = 21007 @Injectable() export class AppleInAppSubscriptionService { constructor(private readonly httpService: HttpService) {} private get password(): string { return globalThis.secret.apple.sharedsecret } async verify(subscription: Subscription): Promise { const latestReceiptsInfo = await this.getReceiptsBySubscription(subscription) return this.getLatestValidReceipts(latestReceiptsInfo).length > 0 } async getInfo(subscription: Subscription): Promise { const latestReceiptInfo = this.getLatestValidReceipts([await this.getLatestReceiptBySubscription(subscription)])[0] if (isNil(latestReceiptInfo)) return null return { expiration: new Date(+latestReceiptInfo.expires_date_ms), trialExpiration: this.extractTrialExperiration(latestReceiptInfo), externalPlan: get(latestReceiptInfo, 'product_id'), externalSubscriptionIdentifier: get(latestReceiptInfo, 'original_transaction_id'), purchasedAt: new Date(+latestReceiptInfo.purchase_date_ms), originalTransaction: latestReceiptInfo.original_transaction_id, receipt: subscription.token, } } async getExternalPlan(subscription: Subscription): Promise { const info = await this.getInfo(subscription) return info?.externalPlan || null } async getExternalSubscriptionIdentifier(subscription: Subscription): Promise { const info = await this.getInfo(subscription) return info?.externalSubscriptionIdentifier || null } async getTrialExpiration(subscription: Subscription): Promise { const info = await this.getInfo(subscription) return info?.trialExpiration || null } async getExpiration(subscription: Subscription): Promise { const info = await this.getInfo(subscription) return info?.expiration || null } private async getLatestReceipt({ receipt, flag = 'buy', }: { receipt: string flag?: string }): Promise> { try { const response = await this.httpService.fetch({ url: this.getUrl(flag), method: 'POST', body: { password: this.password, 'receipt-data': receipt, 'exclude-old-transactions': true, }, name: this.constructor.name, returnStatusCode: true, }) if (response.statusCode === SANDBOX_STATUS) return this.getLatestReceipt({ receipt, flag: 'sandbox' }) return response?.latest_receipt_info } catch (err) { throw new ExternalExceptionError(`Unable to check Apple App Store: ${err}`) } } private getUrl = (flag: string): string => { return `https://${flag}.itunes.apple.com/verifyReceipt` } private async getLatestReceiptBySubscription(subscription: Subscription): Promise { const latestReceiptsInfo = await this.getReceiptsBySubscription(subscription) return last(sortBy(latestReceiptsInfo, receipt => +receipt.purchase_date_ms)) } private getLatestValidReceipts = (latestReceiptsInfo: Array): Array => { return latestReceiptsInfo.filter( (receipt: ReceiptInfo) => new Date(+get(receipt, 'expires_date_ms', 0)) > new Date(), ) } private async getReceiptsBySubscription(subscription: Subscription): Promise> { const receipt = subscription.token if (isEmpty(receipt)) throw new NotFoundError('Apple receipt has not been found on subscription') const latestReceiptInfo = await this.getLatestReceipt({ receipt }) if (isNil(latestReceiptInfo)) throw new NotFoundError('Apple receipt has not been found') return latestReceiptInfo } private extractTrialExperiration = (receipt: ReceiptInfo): Date => Boolean(get(receipt, 'is_trial_period')) || Boolean(get(receipt, 'is_in_intro_offer_period')) ? new Date(+receipt.expires_date_ms) : null }