enhancv/mongoose-subscriptions

View on GitHub
src/Schema/Subscription.js

Summary

Maintainability
C
1 day
Test Coverage
const mongoose = require("mongoose");
const shortid = require("shortid");
const XDate = require("xdate");
const ProcessorItem = require("./ProcessorItem");
const Descriptor = require("./Descriptor");
const originals = require("mongoose-originals");
const Discount = require("./Discount");
const Plan = require("./Plan");
const SubscriptionStatus = require("./Statuses/SubscriptionStatus");

const Subscription = new mongoose.Schema({
    _id: {
        type: String,
        default: shortid.generate,
    },
    processor: {
        type: ProcessorItem,
        default: ProcessorItem,
    },
    plan: {
        type: Plan,
        required: true,
    },
    discounts: [Discount],
    paymentMethodId: String,
    firstBillingDate: Date,
    nextBillingDate: Date,
    paidThroughDate: Date,
    failureCount: Number,
    daysPastDue: Number,
    currentBillingCycle: {
        type: Number,
        min: 0,
        default: 0,
    },
    billingPeriodEndDate: Date,
    billingPeriodStartDate: Date,
    billingDayOfMonth: Number,
    status: {
        type: String,
        enum: SubscriptionStatus.Statuses,
    },
    price: Number,
    statusHistory: [SubscriptionStatus],
    descriptor: Descriptor,
    isTrial: {
        type: Boolean,
        default: false,
    },
    deleted: Boolean,
    trialDuration: Number,
    trialDurationUnit: {
        type: String,
        enum: ["month", "day"],
    },
    createdAt: { type: Date, default: Date.now },
    updatedAt: Date,
});

function addTrial(trialDuration, trialDurationUnit, date) {
    switch (trialDurationUnit) {
        case "day":
            return new XDate(date, true).addDays(trialDuration);
        case "month":
            return new XDate(date, true).addMonths(trialDuration);
        default:
            return date;
    }
}

Subscription.virtual("numberOfFreeBillingCycles").get(function numberOfFreeBillingCycles() {
    return this.discounts.reduce((max, current) => {
        const discountsPrice = this.discounts
            .filter(discount => {
                return current.numberOfBillingcyclesLeft <= discount.numberOfBillingcyclesLeft;
            })
            .map(discount => discount.amount)
            .reduce((sum, value) => sum + value, 0);

        return discountsPrice >= this.price ? current.numberOfBillingcyclesLeft : max;
    }, 0);
});

Subscription.virtual("numberOfFreeMonths").get(function numberOfFreeMonths() {
    return this.numberOfFreeBillingCycles * this.plan.billingFrequency;
});

Subscription.virtual("nextBillingWithFreeDate").get(function numberOfFreeMonths() {
    return this.nextBillingDate
        ? new XDate(this.nextBillingDate, true).addMonths(this.numberOfFreeMonths).toDate()
        : null;
});

Subscription.virtual("firstBillingWithFreeDate").get(function numberOfFreeMonths() {
    return this.firstBillingDate
        ? new XDate(this.firstBillingDate, true).addMonths(this.numberOfFreeMonths).toDate()
        : null;
});

Subscription.virtual("paidThroughWithFreeDate").get(function numberOfFreeMonths() {
    return this.paidThroughDate
        ? new XDate(this.paidThroughDate, true).addMonths(this.numberOfFreeMonths).toDate()
        : null;
});

Subscription.virtual("billingPeriodEndWithFreeDate").get(function numberOfFreeMonths() {
    return this.billingPeriodEndDate
        ? new XDate(this.billingPeriodEndDate, true).addMonths(this.numberOfFreeMonths).toDate()
        : null;
});

Subscription.method("initializeDates", function initializeLocalDates() {
    if (this.processor.state === ProcessorItem.LOCAL) {
        if (this.isTrial) {
            this.firstBillingDate =
                this.firstBillingDate ||
                addTrial(this.trialDuration, this.trialDurationUnit, this.createdAt);
            this.nextBillingDate = this.nextBillingDate || this.firstBillingDate;
            this.billingDayOfMonth = this.billingDayOfMonth || this.nextBillingDate.getDate();
        } else {
            this.firstBillingDate = this.firstBillingDate || this.createdAt;
            this.paidThroughDate =
                this.paidThroughDate ||
                new XDate(this.firstBillingDate, true).addMonths(this.plan.billingFrequency);
            this.billingPeriodStartDate = this.billingPeriodStartDate || this.firstBillingDate;
            this.billingPeriodEndDate = this.billingPeriodEndDate || this.paidThroughDate;
            this.nextBillingDate =
                this.nextBillingDate || new XDate(this.paidThroughDate, true).addDays(1);
            this.billingDayOfMonth = this.billingDayOfMonth || this.nextBillingDate.getDate();
        }
    }
});

Subscription.pre("save", function(next) {
    this.initializeDates();
    next();
});

Subscription.method("addDiscounts", function addDiscounts(callback) {
    const newDiscounts = callback(this);
    const oldDiscounts = this.discounts;

    const allDiscounts = oldDiscounts
        .concat(newDiscounts)
        .filter(item => item)
        .map(item => {
            const itemObject = this.discounts.create(item);
            itemObject.initOriginals();
            return itemObject;
        });

    const preservedDiscounts = allDiscounts.filter(item => item.preserve);

    this.discounts = allDiscounts
        .filter(item => !item.preserve)
        .sort(
            (a, b) =>
                b.amount == a.amount
                    ? b.numberOfBillingcyclesLeft - a.numberOfBillingcyclesLeft
                    : b.amount - a.amount
        )
        .slice(0, 1)
        .concat(preservedDiscounts);

    return this;
});

Subscription.method("inBillingPeriod", function inBillingPeriod(activeDate) {
    const date = activeDate || new Date();
    const startOfDay = new XDate(date, true).clearTime();
    const endOfDay = new XDate(date, true).addDays(1).clearTime();

    if (this.isTrial && startOfDay <= this.firstBillingDate) {
        return true;
    } else if (this.createdAt && this.paidThroughDate) {
        const start = this.createdAt;
        const endDate = new XDate(this.paidThroughWithFreeDate, true).addDays(1);
        return start <= endOfDay && startOfDay <= endDate;
    }
});

Subscription.plugin(originals, {
    fields: [
        "discounts",
        "plan",
        "paymentMethodId",
        "firstBillingDate",
        "paidThroughDate",
        "status",
        "price",
        "descriptor",
        "isTrial",
        "trialDuration",
        "trialDurationUnit",
    ],
});

Subscription.path("discounts").discriminator("DiscountAmount", Discount.DiscountAmount);
Subscription.path("discounts").discriminator("DiscountPercent", Discount.DiscountPercent);
Subscription.path("discounts").discriminator("DiscountCoupon", Discount.DiscountCoupon);
Subscription.path("discounts").discriminator(
    "DiscountCouponRestricted",
    Discount.DiscountCouponRestricted
);
Subscription.path("discounts").discriminator(
    "DiscountPreviousSubscription",
    Discount.DiscountPreviousSubscription
);

module.exports = Subscription;