File size: 4,956 Bytes
8268e91
 
 
 
 
73746a8
 
 
 
426f2a4
73746a8
 
426f2a4
 
73746a8
 
 
 
 
 
 
 
 
 
426f2a4
 
 
 
73746a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f45e448
8268e91
73746a8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8268e91
 
 
 
73746a8
 
 
 
 
 
 
 
 
 
 
 
426f2a4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73746a8
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
import {
  Injectable,
  NotFoundException,
  BadRequestException,
} from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as crypto from 'crypto';
import { Payment, PaymentStatus, PayType } from '../entities/payment.entity';
import { Order, OrderStatus, OrderType } from '../entities/order.entity';
import { UserCourse } from '../entities/user-course.entity';
import { PreparePaymentDto } from './dto/prepare-payment.dto';
import { User } from '../entities/user.entity';
import { Course } from '../entities/course.entity';

@Injectable()
export class PaymentService {
  constructor(
    @InjectRepository(Payment)
    private paymentRepository: Repository<Payment>,
    @InjectRepository(Order)
    private orderRepository: Repository<Order>,
    @InjectRepository(UserCourse)
    private userCourseRepository: Repository<UserCourse>,
    @InjectRepository(User)
    private userRepository: Repository<User>,
    @InjectRepository(Course)
    private courseRepository: Repository<Course>,
  ) {}

  async prepare(userId: number, preparePaymentDto: PreparePaymentDto) {
    const orderIdNum = parseInt(preparePaymentDto.orderId, 10);
    const order = await this.orderRepository.findOne({
      where: { id: orderIdNum, userId },
    });

    if (!order) {
      throw new NotFoundException('Order not found');
    }

    if (order.status !== OrderStatus.PENDING) {
      throw new BadRequestException('Order is not in pending status');
    }

    const paymentNo = `PAY${Date.now()}${Math.floor(Math.random() * 1000000)}`;

    const payment = this.paymentRepository.create({
      orderId: order.id,
      paymentNo,
      payType: preparePaymentDto.payType,
      amount: order.amount,
      status: PaymentStatus.PENDING,
    });

    await this.paymentRepository.save(payment);

    // Mock payment params
    if (preparePaymentDto.payType === PayType.WECHAT) {
      return { payParams: 'mock_wechat_pay_params', paymentNo };
    } else {
      return { orderInfo: 'mock_alipay_order_string', paymentNo };
    }
  }

  // Mock webhook handler
  async handleCallback(paymentNo: string, status: 'SUCCESS' | 'FAILED') {
    const payment = await this.paymentRepository.findOne({
      where: { paymentNo },
      relations: ['order'],
    });

    if (!payment || payment.status !== PaymentStatus.PENDING) {
      return {
        success: false,
        message: 'Invalid payment or already processed',
      };
    }

    if (status === 'SUCCESS') {
      payment.status = PaymentStatus.SUCCESS;
      payment.completedAt = new Date();
      await this.paymentRepository.save(payment);

      const order = payment.order;
      order.status = OrderStatus.PAID;
      order.paidAt = new Date();
      await this.orderRepository.save(order);

      // Only grant access if it's a purchase order
      if (order.orderType === OrderType.PURCHASE) {
        // Send email with drive link instead of generating access token
        const user = await this.userRepository.findOne({ where: { id: order.userId } });
        const course = await this.courseRepository.findOne({ where: { id: order.courseId } });

        if (user && course && course.driveLink) {
          // In a real application, you would use a mail service like Nodemailer here
          console.log('----------------------------------------');
          console.log(`[MOCK EMAIL SERVICE] Sending course materials...`);
          console.log(`To: ${user.email}`);
          console.log(`Subject: 您购买的课程【${course.title}】资料`);
          console.log(`Body: 感谢您的购买!您的课程资料链接如下:\n${course.driveLink}`);
          console.log('----------------------------------------');
        }

        // We still create a UserCourse record to mark that the user has purchased it,
        // but the accessToken isn't strictly needed anymore for viewing the content.
        const expiredAt = new Date();
        expiredAt.setFullYear(expiredAt.getFullYear() + 1); // 1 year access

        const accessToken = crypto.randomBytes(32).toString('hex');

        const userCourse = this.userCourseRepository.create({
          userId: order.userId,
          courseId: order.courseId,
          accessToken,
          expiredAt,
        });

        // Handle duplicate purchase gracefully
        try {
          await this.userCourseRepository.save(userCourse);
        } catch {
          // Already purchased, maybe extend expiration
        }
      } else if (order.orderType === OrderType.VIP) {
        // Upgrade user to VIP
        const user = await this.userRepository.findOne({ where: { id: order.userId } });
        if (user) {
          user.isVip = true;
          await this.userRepository.save(user);
        }
      }
    } else {
      payment.status = PaymentStatus.FAILED;
      await this.paymentRepository.save(payment);
    }

    return { success: true };
  }
}