Browse Source

Implement subscription creation

pull/529/head
Thomas 4 years ago
parent
commit
a396adb153
  1. 17
      apps/api/src/app/subscription/subscription.controller.ts
  2. 22
      apps/api/src/app/subscription/subscription.service.ts
  3. 2
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  4. 37
      apps/client/src/app/pages/account/account-page.component.ts
  5. 9
      apps/client/src/app/pages/account/account-page.html
  6. 4
      apps/client/src/app/pages/account/account-page.module.ts
  7. 9
      apps/client/src/app/pages/account/account-page.scss

17
apps/api/src/app/subscription/subscription.controller.ts

@ -44,7 +44,7 @@ export class SubscriptionController {
); );
} }
const coupons = let coupons =
((await this.propertyService.getByKey(PROPERTY_COUPONS)) as Coupon[]) ?? ((await this.propertyService.getByKey(PROPERTY_COUPONS)) as Coupon[]) ??
[]; [];
@ -59,7 +59,18 @@ export class SubscriptionController {
); );
} }
// TODO: Add subscription await this.subscriptionService.createSubscription(this.request.user.id);
// Destroy coupon
coupons = coupons.filter((coupon) => {
return coupon.code !== couponCode;
});
await this.propertyService.put({
key: PROPERTY_COUPONS,
value: JSON.stringify(coupons)
});
Logger.log(`Coupon with code '${couponCode}' has been redeemed`);
res.status(StatusCodes.OK); res.status(StatusCodes.OK);
@ -71,7 +82,7 @@ export class SubscriptionController {
@Get('stripe/callback') @Get('stripe/callback')
public async stripeCallback(@Req() req, @Res() res) { public async stripeCallback(@Req() req, @Res() res) {
await this.subscriptionService.createSubscription( await this.subscriptionService.createSubscriptionViaStripe(
req.query.checkoutSessionId req.query.checkoutSessionId
); );

22
apps/api/src/app/subscription/subscription.service.ts

@ -2,7 +2,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration.ser
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Subscription } from '@prisma/client'; import { Subscription, User } from '@prisma/client';
import { addDays, isBefore } from 'date-fns'; import { addDays, isBefore } from 'date-fns';
import Stripe from 'stripe'; import Stripe from 'stripe';
@ -64,23 +64,29 @@ export class SubscriptionService {
}; };
} }
public async createSubscription(aCheckoutSessionId: string) { public async createSubscription(aUserId: string) {
try {
const session = await this.stripe.checkout.sessions.retrieve(
aCheckoutSessionId
);
await this.prismaService.subscription.create({ await this.prismaService.subscription.create({
data: { data: {
expiresAt: addDays(new Date(), 365), expiresAt: addDays(new Date(), 365),
User: { User: {
connect: { connect: {
id: session.client_reference_id id: aUserId
} }
} }
} }
}); });
Logger.log(`Subscription for user '${aUserId}' has been created`);
}
public async createSubscriptionViaStripe(aCheckoutSessionId: string) {
try {
const session = await this.stripe.checkout.sessions.retrieve(
aCheckoutSessionId
);
await this.createSubscription(session.client_reference_id);
await this.stripe.customers.update(session.customer as string, { await this.stripe.customers.update(session.customer as string, {
description: session.client_reference_id description: session.client_reference_id
}); });

2
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -236,7 +236,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
} }
private generateCouponCode(aLength: number) { private generateCouponCode(aLength: number) {
const characters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789'; const characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789';
let couponCode = ''; let couponCode = '';
for (let i = 0; i < aLength; i++) { for (let i = 0; i < aLength; i++) {

37
apps/client/src/app/pages/account/account-page.component.ts

@ -10,6 +10,11 @@ import {
MatSlideToggle, MatSlideToggle,
MatSlideToggleChange MatSlideToggleChange
} from '@angular/material/slide-toggle'; } from '@angular/material/slide-toggle';
import {
MatSnackBar,
MatSnackBarRef,
TextOnlySnackBar
} from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto'; import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
@ -18,7 +23,6 @@ import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config'; import { DEFAULT_DATE_FORMAT, baseCurrency } from '@ghostfolio/common/config';
import { Access, User } from '@ghostfolio/common/interfaces'; import { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { StatusCodes } from 'http-status-codes';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { StripeService } from 'ngx-stripe'; import { StripeService } from 'ngx-stripe';
import { EMPTY, Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
@ -50,6 +54,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
public hasPermissionToUpdateUserSettings: boolean; public hasPermissionToUpdateUserSettings: boolean;
public price: number; public price: number;
public priceId: string; public priceId: string;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -62,6 +67,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
private dataService: DataService, private dataService: DataService,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private snackBar: MatSnackBar,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private stripeService: StripeService, private stripeService: StripeService,
@ -187,7 +193,8 @@ export class AccountPageComponent implements OnDestroy, OnInit {
} }
public onRedeemCoupon() { public onRedeemCoupon() {
const couponCode = prompt('Please add your coupon code:'); let couponCode = prompt('Please enter your coupon code:');
couponCode = couponCode?.trim();
if (couponCode) { if (couponCode) {
this.dataService this.dataService
@ -195,13 +202,35 @@ export class AccountPageComponent implements OnDestroy, OnInit {
.pipe( .pipe(
takeUntil(this.unsubscribeSubject), takeUntil(this.unsubscribeSubject),
catchError(() => { catchError(() => {
// TODO: show error notification this.snackBar.open('😞 Could not redeem coupon code', undefined, {
duration: 3000
});
return EMPTY; return EMPTY;
}) })
) )
.subscribe(() => { .subscribe(() => {
// TODO: show success notification this.snackBarRef = this.snackBar.open(
'✅ Coupon code has been redeemed',
'Reload',
{
duration: 3000
}
);
this.snackBarRef
.afterDismissed()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
window.location.reload();
});
this.snackBarRef
.onAction()
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
window.location.reload();
});
}); });
} }
} }

9
apps/client/src/app/pages/account/account-page.html

@ -47,14 +47,13 @@
<ng-container *ngIf="!coupon">{{ price }}</ng-container> <ng-container *ngIf="!coupon">{{ price }}</ng-container>
<span i18n> per year</span> <span i18n> per year</span>
</div> </div>
<button <a
color="primary" class="cursor-pointer d-block mt-2"
i18n i18n
mat-button [routerLink]=""
(click)="onRedeemCoupon()" (click)="onRedeemCoupon()"
>Redeem Coupon</a
> >
Redeem Coupon
</button>
</div> </div>
</div> </div>
</div> </div>

4
apps/client/src/app/pages/account/account-page.module.ts

@ -8,6 +8,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module'; import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
import { AccountPageRoutingModule } from './account-page-routing.module'; import { AccountPageRoutingModule } from './account-page-routing.module';
@ -30,7 +31,8 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
MatInputModule, MatInputModule,
MatSelectModule, MatSelectModule,
MatSlideToggleModule, MatSlideToggleModule,
ReactiveFormsModule ReactiveFormsModule,
RouterModule
], ],
providers: [] providers: []
}) })

9
apps/client/src/app/pages/account/account-page.scss

@ -2,6 +2,15 @@
color: rgb(var(--dark-primary-text)); color: rgb(var(--dark-primary-text));
display: block; display: block;
a {
color: rgba(var(--palette-primary-500), 1);
font-weight: 500;
&:hover {
color: rgba(var(--palette-primary-300), 1);
}
}
gf-access-table { gf-access-table {
overflow-x: auto; overflow-x: auto;

Loading…
Cancel
Save