Feature/add support for coupon duration (#743)
* Add support for coupon duration * Update changelog
This commit is contained in:
parent
b602e7690b
commit
99655604d9
@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for setting a duration in the coupon system
|
||||
|
||||
### Changed
|
||||
|
||||
- Upgraded `ngx-skeleton-loader` from version `2.9.1` to `5.0.0`
|
||||
|
@ -1,10 +1,10 @@
|
||||
import { AccountService } from '@ghostfolio/api/app/account/account.service';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
import { OrderService } from '@ghostfolio/api/app/order/order.service';
|
||||
import { ConfigurationService } from '@ghostfolio/api/services/configuration.service';
|
||||
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isSameDay, parseISO } from 'date-fns';
|
||||
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
|
||||
|
||||
@Injectable()
|
||||
export class ImportService {
|
||||
|
@ -46,22 +46,25 @@ export class SubscriptionController {
|
||||
((await this.propertyService.getByKey(PROPERTY_COUPONS)) as Coupon[]) ??
|
||||
[];
|
||||
|
||||
const isValid = coupons.some((coupon) => {
|
||||
return coupon.code === couponCode;
|
||||
const coupon = coupons.find((currentCoupon) => {
|
||||
return currentCoupon.code === couponCode;
|
||||
});
|
||||
|
||||
if (!isValid) {
|
||||
if (coupon === undefined) {
|
||||
throw new HttpException(
|
||||
getReasonPhrase(StatusCodes.BAD_REQUEST),
|
||||
StatusCodes.BAD_REQUEST
|
||||
);
|
||||
}
|
||||
|
||||
await this.subscriptionService.createSubscription(this.request.user.id);
|
||||
await this.subscriptionService.createSubscription({
|
||||
duration: coupon.duration,
|
||||
userId: this.request.user.id
|
||||
});
|
||||
|
||||
// Destroy coupon
|
||||
coupons = coupons.filter((coupon) => {
|
||||
return coupon.code !== couponCode;
|
||||
coupons = coupons.filter((currentCoupon) => {
|
||||
return currentCoupon.code !== couponCode;
|
||||
});
|
||||
await this.propertyService.put({
|
||||
key: PROPERTY_COUPONS,
|
||||
@ -69,7 +72,7 @@ export class SubscriptionController {
|
||||
});
|
||||
|
||||
Logger.log(
|
||||
`Subscription for user '${this.request.user.id}' has been created with coupon`
|
||||
`Subscription for user '${this.request.user.id}' has been created with a coupon for ${coupon.duration}`
|
||||
);
|
||||
|
||||
return {
|
||||
|
@ -2,8 +2,9 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Subscription, User } from '@prisma/client';
|
||||
import { addDays, isBefore } from 'date-fns';
|
||||
import { Subscription } from '@prisma/client';
|
||||
import { addMilliseconds, isBefore } from 'date-fns';
|
||||
import ms, { StringValue } from 'ms';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
@Injectable()
|
||||
@ -64,13 +65,19 @@ export class SubscriptionService {
|
||||
};
|
||||
}
|
||||
|
||||
public async createSubscription(aUserId: string) {
|
||||
public async createSubscription({
|
||||
duration = '1 year',
|
||||
userId
|
||||
}: {
|
||||
duration?: StringValue;
|
||||
userId: string;
|
||||
}) {
|
||||
await this.prismaService.subscription.create({
|
||||
data: {
|
||||
expiresAt: addDays(new Date(), 365),
|
||||
expiresAt: addMilliseconds(new Date(), ms(duration)),
|
||||
User: {
|
||||
connect: {
|
||||
id: aUserId
|
||||
id: userId
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -83,7 +90,7 @@ export class SubscriptionService {
|
||||
aCheckoutSessionId
|
||||
);
|
||||
|
||||
await this.createSubscription(session.client_reference_id);
|
||||
await this.createSubscription({ userId: session.client_reference_id });
|
||||
|
||||
await this.stripe.customers.update(session.customer as string, {
|
||||
description: session.client_reference_id
|
||||
|
@ -20,6 +20,7 @@ import {
|
||||
parseISO
|
||||
} from 'date-fns';
|
||||
import { uniq } from 'lodash';
|
||||
import { StringValue } from 'ms';
|
||||
import { Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -29,6 +30,7 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './admin-overview.html'
|
||||
})
|
||||
export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
public couponDuration: StringValue = '30 days';
|
||||
public coupons: Coupon[];
|
||||
public customCurrencies: string[];
|
||||
public dataGatheringInProgress: boolean;
|
||||
@ -105,7 +107,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
|
||||
public onAddCoupon() {
|
||||
const coupons = [...this.coupons, { code: this.generateCouponCode(16) }];
|
||||
const coupons = [
|
||||
...this.coupons,
|
||||
{ code: this.generateCouponCode(16), duration: this.couponDuration }
|
||||
];
|
||||
this.putCoupons(coupons);
|
||||
}
|
||||
|
||||
@ -118,6 +123,10 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
public onChangeCouponDuration(aCouponDuration: StringValue) {
|
||||
this.couponDuration = aCouponDuration;
|
||||
}
|
||||
|
||||
public onDeleteCoupon(aCouponCode: string) {
|
||||
const confirmation = confirm('Do you really want to delete this coupon?');
|
||||
|
||||
|
@ -156,11 +156,14 @@
|
||||
></mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
<div *ngIf="hasPermissionForSubscription" class="d-flex my-3">
|
||||
<div
|
||||
*ngIf="hasPermissionForSubscription"
|
||||
class="d-flex my-3 subscription"
|
||||
>
|
||||
<div class="w-50" i18n>Coupons</div>
|
||||
<div class="w-50">
|
||||
<div *ngFor="let coupon of coupons">
|
||||
<span>{{ coupon.code }}</span>
|
||||
<span>{{ coupon.code }} ({{ coupon.duration }})</span>
|
||||
<button
|
||||
class="mini-icon mx-1 no-min-width px-2"
|
||||
mat-button
|
||||
@ -170,10 +173,25 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<button color="primary" mat-flat-button (click)="onAddCoupon()">
|
||||
<ion-icon class="mr-1" name="add-outline"></ion-icon>
|
||||
<span i18n>Add Coupon</span>
|
||||
<form #couponForm="ngForm">
|
||||
<mat-form-field appearance="outline" class="mr-2">
|
||||
<mat-select
|
||||
name="duration"
|
||||
[value]="couponDuration"
|
||||
(selectionChange)="onChangeCouponDuration($event.value)"
|
||||
>
|
||||
<mat-option value="30 days">30 Days</mat-option>
|
||||
<mat-option value="1 year">1 Year</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<button
|
||||
color="primary"
|
||||
mat-flat-button
|
||||
(click)="onAddCoupon()"
|
||||
>
|
||||
<span i18n>Add</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,7 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { CacheService } from '@ghostfolio/client/services/cache.service';
|
||||
import { GfValueModule } from '@ghostfolio/ui/value';
|
||||
@ -12,11 +14,14 @@ import { AdminOverviewComponent } from './admin-overview.component';
|
||||
declarations: [AdminOverviewComponent],
|
||||
exports: [],
|
||||
imports: [
|
||||
FormsModule,
|
||||
CommonModule,
|
||||
GfValueModule,
|
||||
MatButtonModule,
|
||||
MatCardModule,
|
||||
MatSlideToggleModule
|
||||
MatSelectModule,
|
||||
MatSlideToggleModule,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
providers: [CacheService],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
|
@ -20,4 +20,10 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscription {
|
||||
.mat-form-field {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,3 +1,6 @@
|
||||
import { StringValue } from 'ms';
|
||||
|
||||
export interface Coupon {
|
||||
code: string;
|
||||
duration?: StringValue;
|
||||
}
|
||||
|
@ -100,6 +100,7 @@
|
||||
"http-status-codes": "2.2.0",
|
||||
"ionicons": "5.5.1",
|
||||
"lodash": "4.17.21",
|
||||
"ms": "3.0.0-canary.1",
|
||||
"ngx-device-detector": "3.0.0",
|
||||
"ngx-markdown": "13.0.0",
|
||||
"ngx-skeleton-loader": "5.0.0",
|
||||
|
@ -13556,6 +13556,11 @@ ms@2.1.2:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ms@3.0.0-canary.1:
|
||||
version "3.0.0-canary.1"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-3.0.0-canary.1.tgz#c7b34fbce381492fd0b345d1cf56e14d67b77b80"
|
||||
integrity sha512-kh8ARjh8rMN7Du2igDRO9QJnqCb2xYTJxyQYK7vJJS4TvLLmsbyhiKpSW+t+y26gyOyMd0riphX0GeWKU3ky5g==
|
||||
|
||||
ms@^2.0.0, ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
|
Loading…
x
Reference in New Issue
Block a user