Mail Modul überarbeitet, Korrekturen am PaymentService, neuer customerType seller
This commit is contained in:
parent
b4609d07ba
commit
7a286e3519
|
|
@ -14,7 +14,8 @@
|
|||
"console": "integratedTerminal",
|
||||
"env": {
|
||||
"HOST_NAME": "localhost"
|
||||
}
|
||||
},
|
||||
"preLaunchTask": "Start Stripe Listener"
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
|
|
@ -60,5 +61,30 @@
|
|||
"sourceMaps": true,
|
||||
"smartStep": true
|
||||
}
|
||||
],
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Start Stripe Listener",
|
||||
"type": "shell",
|
||||
"command": "stripe listen -e checkout.session.completed --forward-to http://localhost:3000/bizmatch/payment/webhook",
|
||||
"isBackground": true,
|
||||
"problemMatcher": [
|
||||
{
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": ".",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": ".",
|
||||
"endsPattern": "."
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "Start Stripe Listener",
|
||||
"type": "shell",
|
||||
"command": "stripe listen -e checkout.session.completed --forward-to http://localhost:3000/bizmatch/payment/webhook",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true, // Task läuft im Hintergrund
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Start Nest.js",
|
||||
"type": "npm",
|
||||
"script": "start:debug",
|
||||
"isBackground": false,
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"echo": true,
|
||||
"reveal": "always",
|
||||
"focus": false,
|
||||
"panel": "shared"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -2,7 +2,7 @@ import { boolean, char, doublePrecision, integer, jsonb, pgEnum, pgTable, serial
|
|||
import { AreasServed, LicensedIn } from '../models/db.model';
|
||||
export const PG_CONNECTION = 'PG_CONNECTION';
|
||||
export const genderEnum = pgEnum('gender', ['male', 'female']);
|
||||
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'professional']);
|
||||
export const customerTypeEnum = pgEnum('customerType', ['buyer', 'seller', 'professional']);
|
||||
export const customerSubTypeEnum = pgEnum('customerSubType', ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
export const listingsCategoryEnum = pgEnum('listingsCategory', ['commercialProperty', 'business']);
|
||||
export const subscriptionTypeEnum = pgEnum('subscriptionType', ['free', 'professional', 'broker']);
|
||||
|
|
@ -33,8 +33,6 @@ export const users = pgTable('users', {
|
|||
longitude: doublePrecision('longitude'),
|
||||
stripeCustomerId: text('stripeCustomerId'),
|
||||
subscriptionId: text('subscriptionId'),
|
||||
planActive: boolean('planActive').default(false),
|
||||
planExpires: timestamp('planExpires'),
|
||||
subscriptionPlan: subscriptionTypeEnum('subscriptionType'),
|
||||
// embedding: vector('embedding', { dimensions: 1536 }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
import { registerAs } from '@nestjs/config';
|
||||
|
||||
export default registerAs('mail', () => ({
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
port: 587,
|
||||
secure: false,
|
||||
auth: {
|
||||
user: process.env.AMAZON_USER,
|
||||
pass: process.env.AMAZON_PASSWORD,
|
||||
},
|
||||
defaults: {
|
||||
from: '"No Reply" <noreply@example.com>',
|
||||
},
|
||||
}));
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { User } from 'src/models/db.model';
|
||||
import { ErrorResponse, MailInfo } from '../models/main.model';
|
||||
import { MailService } from './mail.service.js';
|
||||
|
||||
|
|
@ -13,4 +14,8 @@ export class MailController {
|
|||
return this.mailService.sendRequest(mailInfo);
|
||||
}
|
||||
}
|
||||
@Post('subscriptionConfirmation')
|
||||
sendSubscriptionConfirmation(@Body() user: User): Promise<void | ErrorResponse> {
|
||||
return this.mailService.sendSubscriptionConfirmation(user);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,33 +13,67 @@ import { MailController } from './mail.controller.js';
|
|||
import { MailService } from './mail.service.js';
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const user = process.env.amazon_user;
|
||||
const password = process.env.amazon_password;
|
||||
const user = process.env.AMAZON_USER;
|
||||
const password = process.env.AMAZON_PASSWORD;
|
||||
console.log('Amazon User:', process.env.AMAZON_USER);
|
||||
console.log('Amazon Password:', process.env.AMAZON_PASSWORD);
|
||||
@Module({
|
||||
imports: [
|
||||
DrizzleModule,
|
||||
UserModule,
|
||||
GeoModule,
|
||||
MailerModule.forRoot({
|
||||
transport: {
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
secure: false,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: 'AKIAU6GDWVAQ2QNFLNWN',
|
||||
pass: 'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
// ConfigModule.forFeature(mailConfig),
|
||||
// MailerModule.forRoot({
|
||||
// transport: {
|
||||
// host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
// secure: false,
|
||||
// port: 587,
|
||||
// auth: {
|
||||
// user: user, //'AKIAU6GDWVAQ2QNFLNWN',
|
||||
// pass: password, //'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
// },
|
||||
// },
|
||||
// defaults: {
|
||||
// from: '"No Reply" <noreply@example.com>',
|
||||
// },
|
||||
// template: {
|
||||
// dir: join(__dirname, 'templates'),
|
||||
// adapter: new HandlebarsAdapter({
|
||||
// eq: function (a, b) {
|
||||
// return a === b;
|
||||
// },
|
||||
// }),
|
||||
// options: {
|
||||
// strict: true,
|
||||
// },
|
||||
// },
|
||||
// }),
|
||||
MailerModule.forRootAsync({
|
||||
useFactory: () => ({
|
||||
transport: {
|
||||
host: 'email-smtp.us-east-2.amazonaws.com',
|
||||
secure: false,
|
||||
port: 587,
|
||||
auth: {
|
||||
user: process.env.AMAZON_USER, //'AKIAU6GDWVAQ2QNFLNWN',
|
||||
pass: process.env.AMAZON_PASSWORD, //'BDE9nZv/ARbpotim1mIOir52WgIbpSi9cv1oJoH8oEf7',
|
||||
},
|
||||
},
|
||||
},
|
||||
defaults: {
|
||||
from: '"No Reply" <noreply@example.com>',
|
||||
},
|
||||
template: {
|
||||
dir: join(__dirname, 'templates'),
|
||||
adapter: new HandlebarsAdapter(), // or new PugAdapter() or new EjsAdapter()
|
||||
options: {
|
||||
strict: true,
|
||||
defaults: {
|
||||
from: '"No Reply" <noreply@example.com>',
|
||||
},
|
||||
},
|
||||
template: {
|
||||
dir: join(__dirname, 'templates'),
|
||||
adapter: new HandlebarsAdapter({
|
||||
eq: function (a, b) {
|
||||
return a === b;
|
||||
},
|
||||
}),
|
||||
options: {
|
||||
strict: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
}),
|
||||
],
|
||||
providers: [MailService, UserService, FileService, GeoService],
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ export class MailService {
|
|||
}
|
||||
async sendSubscriptionConfirmation(user: User): Promise<void> {
|
||||
await this.mailerService.sendMail({
|
||||
to: 'support@bizmatch.net',
|
||||
to: user.email,
|
||||
from: `"Bizmatch Support Team" <info@bizmatch.net>`,
|
||||
subject: `Subscription Confirmation`,
|
||||
//template: './inquiry', // `.hbs` extension is appended automatically
|
||||
|
|
|
|||
|
|
@ -17,18 +17,18 @@ export interface UserData {
|
|||
hasCompanyLogo?: boolean;
|
||||
licensedIn?: string[];
|
||||
gender?: 'male' | 'female';
|
||||
customerType?: 'buyer' | 'broker' | 'professional';
|
||||
customerType?: 'buyer' | 'seller' | 'professional';
|
||||
customerSubType?: 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||
created?: Date;
|
||||
updated?: Date;
|
||||
}
|
||||
export type Gender = 'male' | 'female';
|
||||
export type CustomerType = 'buyer' | 'professional';
|
||||
export type CustomerType = 'buyer' | 'seller' | 'professional';
|
||||
export type CustomerSubType = 'broker' | 'cpa' | 'attorney' | 'titleCompany' | 'surveyor' | 'appraiser';
|
||||
export type ListingsCategory = 'commercialProperty' | 'business';
|
||||
|
||||
export const GenderEnum = z.enum(['male', 'female']);
|
||||
export const CustomerTypeEnum = z.enum(['buyer', 'professional']);
|
||||
export const CustomerTypeEnum = z.enum(['buyer', 'seller', 'professional']);
|
||||
export const SubscriptionTypeEnum = z.enum(['free', 'professional', 'broker']);
|
||||
export const CustomerSubTypeEnum = z.enum(['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser']);
|
||||
export const ListingsCategoryEnum = z.enum(['commercialProperty', 'business']);
|
||||
|
|
|
|||
|
|
@ -58,23 +58,19 @@ export class PaymentService {
|
|||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: 'http://localhost:4200/success',
|
||||
cancel_url: 'http://localhost:4200/pricing',
|
||||
success_url: `${process.env.WEB_HOST}/success`,
|
||||
cancel_url: `${process.env.WEB_HOST}/pricing`,
|
||||
// customer_email: checkout.email,
|
||||
customer: customerId,
|
||||
// customer_details:{
|
||||
// name: checkout.name, // Hier wird der Name des Kunden übergeben
|
||||
// },
|
||||
shipping_address_collection: {
|
||||
allowed_countries: ['US'],
|
||||
},
|
||||
|
||||
client_reference_id: '1234',
|
||||
client_reference_id: btoa(checkout.name),
|
||||
locale: 'en',
|
||||
subscription_data: {
|
||||
trial_end: Math.floor(new Date().setMonth(new Date().getMonth() + 3) / 1000),
|
||||
metadata: { plan: product.name },
|
||||
},
|
||||
metadata: { plan: product.name },
|
||||
});
|
||||
return session;
|
||||
} else {
|
||||
|
|
@ -100,20 +96,11 @@ export class PaymentService {
|
|||
});
|
||||
user.stripeCustomerId = session.customer as string;
|
||||
user.subscriptionId = session.subscription as string;
|
||||
user.planActive = true;
|
||||
//session.shipping_details ->
|
||||
// "shipping_details": {
|
||||
// "address": {
|
||||
// "city": "Springfield",
|
||||
// "country": "US",
|
||||
// "line1": "West Maple Avenue South",
|
||||
// "line2": null,
|
||||
// "postal_code": "62704",
|
||||
// "state": "IL"
|
||||
// },
|
||||
// "name": "Johnathan Miller"
|
||||
// }
|
||||
user.subscriptionPlan = session.amount_total === 4900 ? 'broker' : 'professional'; //session.metadata['subscriptionPlan'] as 'free' | 'professional' | 'broker';
|
||||
user.customerType = 'professional';
|
||||
if (session.metadata['plan'] === 'Broker Plan') {
|
||||
user.customerSubType = 'broker';
|
||||
}
|
||||
user.subscriptionPlan = session.metadata['plan'] === 'Broker Plan' ? 'broker' : 'professional'; //session.metadata['subscriptionPlan'] as 'free' | 'professional' | 'broker';
|
||||
this.userService.saveUser(user);
|
||||
this.mailService.sendSubscriptionConfirmation(user);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ export class SelectOptionsService {
|
|||
];
|
||||
public customerTypes: Array<KeyValue> = [
|
||||
{ name: 'Buyer', value: 'buyer' },
|
||||
{ name: 'Commercial Property Seller', value: 'seller' },
|
||||
{ name: 'Professional', value: 'professional' },
|
||||
];
|
||||
public customerSubTypes: Array<KeyValue> = [
|
||||
|
|
|
|||
|
|
@ -11,7 +11,15 @@
|
|||
<app-tooltip id="tooltip-{{ name }}" [text]="validationMessage"></app-tooltip>
|
||||
}
|
||||
</label>
|
||||
<select [id]="name" [name]="name" [ngModel]="value" (change)="onSelectChange($event)" (blur)="onTouched()" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500">
|
||||
<select
|
||||
[disabled]="disabled"
|
||||
[id]="name"
|
||||
[name]="name"
|
||||
[ngModel]="value"
|
||||
(change)="onSelectChange($event)"
|
||||
(blur)="onTouched()"
|
||||
class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-indigo-500 focus:ring-indigo-500"
|
||||
>
|
||||
<option value="" disabled selected>Select an option</option>
|
||||
<option *ngFor="let option of options" [value]="option.value">
|
||||
{{ option.label }}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import { ValidationMessagesService } from '../validation-messages.service';
|
|||
})
|
||||
export class ValidatedSelectComponent extends BaseInputComponent {
|
||||
@Input() options: Array<{ value: any; label: string }> = [];
|
||||
@Input() disabled = false;
|
||||
@Output() valueChange = new EventEmitter<any>();
|
||||
|
||||
constructor(validationMessagesService: ValidationMessagesService) {
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ export class PricingComponent {
|
|||
await this.userService.save(user);
|
||||
this.router.navigate([`/account`]);
|
||||
} else {
|
||||
this.checkout({ priceId: atob(this.id), email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
|
||||
this.checkout({ priceId: priceId, email: this.keycloakUser.email, name: `${this.keycloakUser.firstName} ${this.keycloakUser.lastName}` });
|
||||
}
|
||||
} else {
|
||||
if (priceId) {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
</div>
|
||||
|
||||
}@else{
|
||||
<app-validated-select label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
|
||||
<app-validated-select [disabled]="user.customerType === 'professional'" label="Customer Type" name="customerType" [(ngModel)]="user.customerType" [options]="customerTypeOptions"></app-validated-select>
|
||||
} @if (isProfessional){
|
||||
<!-- <div>
|
||||
<label for="customerSubType" class="block text-sm font-medium text-gray-700">Professional Type</label>
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
<option *ngFor="let subType of customerSubTypes" [value]="subType">{{ subType | titlecase }}</option>
|
||||
</select>
|
||||
</div> -->
|
||||
<app-validated-select label="Professional Type" name="customerSubType" [(ngModel)]="user.customerSubType" [options]="customerSubTypeOptions"></app-validated-select>
|
||||
<app-validated-select [disabled]="user.customerSubType === 'broker'" label="Professional Type" name="customerSubType" [(ngModel)]="user.customerSubType" [options]="customerSubTypeOptions"></app-validated-select>
|
||||
}
|
||||
</div>
|
||||
@if (isProfessional){
|
||||
|
|
@ -246,39 +246,39 @@
|
|||
<div class="flex justify-start">
|
||||
<button routerLink="/pricing" class="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">Upgrade Subscription Plan</button>
|
||||
</div>
|
||||
<!-- <div class="mt-8 sm:hidden">
|
||||
<div class="mt-8 sm:hidden">
|
||||
<h3 class="text-lg font-medium text-gray-700 mb-1">Membership Level</h3>
|
||||
<div class="space-y-2">
|
||||
@for (subscription of userSubscriptions; track userSubscriptions){
|
||||
@for (subscription of subscriptions; track subscriptions; let i=$index){
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||
<div class="px-4 py-5 sm:px-6">
|
||||
<dl class="grid grid-cols-1 gap-x-4 gap-y-2 sm:grid-cols-2">
|
||||
<div class="sm:col-span-1 flex">
|
||||
<dt class="text-sm font-bold text-gray-500 mr-2">Level</dt>
|
||||
<dd class="text-sm text-gray-900">{{ level }}</dd>
|
||||
<dd class="text-sm text-gray-900">{{ getLevel(i) }}</dd>
|
||||
</div>
|
||||
<div class="sm:col-span-1 flex">
|
||||
<dt class="text-sm font-bold text-gray-500 mr-2">Start Date</dt>
|
||||
<dd class="text-sm text-gray-900">{{ subscription.start | date }}</dd>
|
||||
</div>
|
||||
<div class="sm:col-span-1 flex">
|
||||
<dt class="text-sm font-bold text-gray-500 mr-2">Date Modified</dt>
|
||||
<dd class="text-sm text-gray-900">{{ subscription.modified | date }}</dd>
|
||||
<dd class="text-sm text-gray-900">{{ getStartDate(i) }}</dd>
|
||||
</div>
|
||||
<div class="sm:col-span-1 flex">
|
||||
<dt class="text-sm font-bold text-gray-500 mr-2">End Date</dt>
|
||||
<dd class="text-sm text-gray-900">{{ subscription.end | date }}</dd>
|
||||
<dd class="text-sm text-gray-900">{{ getEndDate(i) }}</dd>
|
||||
</div>
|
||||
<div class="sm:col-span-1 flex">
|
||||
<dt class="text-sm font-bold text-gray-500 mr-2">Next Settlement</dt>
|
||||
<dd class="text-sm text-gray-900">{{ getNextSettlement(i) }}</dd>
|
||||
</div>
|
||||
<div class="sm:col-span-1 flex">
|
||||
<dt class="text-sm font-bold text-gray-500 mr-2">Status</dt>
|
||||
<dd class="text-sm text-gray-900">{{ subscription.status }}</dd>
|
||||
<dd class="text-sm text-gray-900">{{ getStatus(i) }}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -67,8 +67,6 @@ export class AccountComponent {
|
|||
editorModules = TOOLBAR_OPTIONS;
|
||||
env = environment;
|
||||
faTrash = faTrash;
|
||||
customerTypes = ['buyer', 'professional'];
|
||||
customerSubTypes = ['broker', 'cpa', 'attorney', 'titleCompany', 'surveyor', 'appraiser'];
|
||||
quillModules = {
|
||||
toolbar: [['bold', 'italic', 'underline', 'strike'], [{ list: 'ordered' }, { list: 'bullet' }], [{ header: [1, 2, 3, 4, 5, 6, false] }], [{ color: [] }, { background: [] }], ['clean']],
|
||||
};
|
||||
|
|
@ -116,15 +114,19 @@ export class AccountComponent {
|
|||
this.profileUrl = this.user.hasProfile ? `${this.env.imageBaseUrl}/pictures/profile/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||
this.companyLogoUrl = this.user.hasCompanyLogo ? `${this.env.imageBaseUrl}/pictures/logo/${emailToDirName(this.user.email)}.avif?_ts=${new Date().getTime()}` : `/assets/images/placeholder.png`;
|
||||
|
||||
this.customerTypeOptions = this.customerTypes.map(type => ({
|
||||
value: type,
|
||||
label: this.titleCasePipe.transform(type),
|
||||
}));
|
||||
this.customerTypeOptions = this.selectOptions.customerTypes
|
||||
.filter(ct => ct.value === 'buyer' || ct.value === 'seller' || this.user.customerType === 'professional')
|
||||
.map(type => ({
|
||||
value: type.value,
|
||||
label: this.titleCasePipe.transform(type.name),
|
||||
}));
|
||||
|
||||
this.customerSubTypeOptions = this.customerSubTypes.map(type => ({
|
||||
value: type,
|
||||
label: this.titleCasePipe.transform(type),
|
||||
}));
|
||||
this.customerSubTypeOptions = this.selectOptions.customerSubTypes
|
||||
.filter(ct => ct.value !== 'broker' || this.user.customerSubType === 'broker')
|
||||
.map(type => ({
|
||||
value: type.value,
|
||||
label: this.titleCasePipe.transform(type.name),
|
||||
}));
|
||||
}
|
||||
ngOnDestroy() {
|
||||
this.validationMessagesService.clearMessages(); // Löschen Sie alle bestehenden Validierungsnachrichten
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export class SuccessComponent {
|
|||
}
|
||||
|
||||
let attempts = 0;
|
||||
const maxAttempts = 5;
|
||||
const maxAttempts = 20;
|
||||
const interval = 5000; // 5 Sekunden
|
||||
|
||||
const intervalId = setInterval(async () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue