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[]) ??
[];
@ -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);
@ -71,7 +82,7 @@ export class SubscriptionController {
@Get('stripe/callback')
public async stripeCallback(@Req() req, @Res() res) {
await this.subscriptionService.createSubscription(
await this.subscriptionService.createSubscriptionViaStripe(
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 { SubscriptionType } from '@ghostfolio/common/types/subscription.type';
import { Injectable, Logger } from '@nestjs/common';
import { Subscription } from '@prisma/client';
import { Subscription, User } from '@prisma/client';
import { addDays, isBefore } from 'date-fns';
import Stripe from 'stripe';
@ -64,23 +64,29 @@ export class SubscriptionService {
};
}
public async createSubscription(aCheckoutSessionId: string) {
try {
const session = await this.stripe.checkout.sessions.retrieve(
aCheckoutSessionId
);
public async createSubscription(aUserId: string) {
await this.prismaService.subscription.create({
data: {
expiresAt: addDays(new Date(), 365),
User: {
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, {
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) {
const characters = 'ABCDEFGHIJKLMNPQRSTUVWXYZ123456789';
const characters = 'ABCDEFGHJKLMNPQRSTUVWXYZ123456789';
let couponCode = '';
for (let i = 0; i < aLength; i++) {

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

@ -10,6 +10,11 @@ import {
MatSlideToggle,
MatSlideToggleChange
} from '@angular/material/slide-toggle';
import {
MatSnackBar,
MatSnackBarRef,
TextOnlySnackBar
} from '@angular/material/snack-bar';
import { ActivatedRoute, Router } from '@angular/router';
import { CreateAccessDto } from '@ghostfolio/api/app/access/create-access.dto';
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 { Access, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { StatusCodes } from 'http-status-codes';
import { DeviceDetectorService } from 'ngx-device-detector';
import { StripeService } from 'ngx-stripe';
import { EMPTY, Subject } from 'rxjs';
@ -50,6 +54,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
public hasPermissionToUpdateUserSettings: boolean;
public price: number;
public priceId: string;
public snackBarRef: MatSnackBarRef<TextOnlySnackBar>;
public user: User;
private unsubscribeSubject = new Subject<void>();
@ -62,6 +67,7 @@ export class AccountPageComponent implements OnDestroy, OnInit {
private dataService: DataService,
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private snackBar: MatSnackBar,
private route: ActivatedRoute,
private router: Router,
private stripeService: StripeService,
@ -187,7 +193,8 @@ export class AccountPageComponent implements OnDestroy, OnInit {
}
public onRedeemCoupon() {
const couponCode = prompt('Please add your coupon code:');
let couponCode = prompt('Please enter your coupon code:');
couponCode = couponCode?.trim();
if (couponCode) {
this.dataService
@ -195,13 +202,35 @@ export class AccountPageComponent implements OnDestroy, OnInit {
.pipe(
takeUntil(this.unsubscribeSubject),
catchError(() => {
// TODO: show error notification
this.snackBar.open('😞 Could not redeem coupon code', undefined, {
duration: 3000
});
return EMPTY;
})
)
.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>
<span i18n> per year</span>
</div>
<button
color="primary"
<a
class="cursor-pointer d-block mt-2"
i18n
mat-button
[routerLink]=""
(click)="onRedeemCoupon()"
>Redeem Coupon</a
>
Redeem Coupon
</button>
</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 { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router';
import { GfPortfolioAccessTableModule } from '@ghostfolio/client/components/access-table/access-table.module';
import { AccountPageRoutingModule } from './account-page-routing.module';
@ -30,7 +31,8 @@ import { GfCreateOrUpdateAccessDialogModule } from './create-or-update-access-di
MatInputModule,
MatSelectModule,
MatSlideToggleModule,
ReactiveFormsModule
ReactiveFormsModule,
RouterModule
],
providers: []
})

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

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

Loading…
Cancel
Save