feat(sprint-6): Phase 3 — Stripe integration (SEPA + PayPal + Card)
- V7 migration: subscriptions table with plan tiers - Subscription entity + PlanTier/SubscriptionStatus enums - StripeService: customer creation, checkout, portal, webhook handling - SubscriptionController: /api/v1/billing endpoints - Webhook handler: invoice.paid, payment_failed, subscription.deleted/updated - Plan enforcement: member limit interceptor, trial expiry check - Frontend: /settings/billing page (plan card, usage, upgrade, portal link) - Trial expired banner on all admin pages - React Query hooks (useSubscriptionQuery, checkout/portal mutations) - Stripe Java SDK 28.2.0 - Full i18n (de/en) for billing namespace
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import de.cannamanage.domain.enums.PlanTier;
|
||||
import de.cannamanage.domain.enums.SubscriptionStatus;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "subscriptions")
|
||||
public class Subscription extends AbstractTenantEntity {
|
||||
|
||||
@Column(name = "club_id", nullable = false, unique = true)
|
||||
private UUID clubId;
|
||||
|
||||
@Column(name = "stripe_customer_id", nullable = false)
|
||||
private String stripeCustomerId;
|
||||
|
||||
@Column(name = "stripe_subscription_id")
|
||||
private String stripeSubscriptionId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "plan_tier", nullable = false, length = 20)
|
||||
private PlanTier planTier = PlanTier.TRIAL;
|
||||
|
||||
@Column(name = "member_limit", nullable = false)
|
||||
private Integer memberLimit = 500;
|
||||
|
||||
@Column(name = "trial_ends_at")
|
||||
private Instant trialEndsAt;
|
||||
|
||||
@Column(name = "current_period_start")
|
||||
private Instant currentPeriodStart;
|
||||
|
||||
@Column(name = "current_period_end")
|
||||
private Instant currentPeriodEnd;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private SubscriptionStatus status = SubscriptionStatus.TRIALING;
|
||||
|
||||
@Column(name = "canceled_at")
|
||||
private Instant canceledAt;
|
||||
|
||||
@Column(name = "updated_at")
|
||||
private Instant updatedAt;
|
||||
|
||||
@PrePersist
|
||||
@Override
|
||||
void onCreate() {
|
||||
super.onCreate();
|
||||
this.updatedAt = Instant.now();
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
void onUpdate() {
|
||||
this.updatedAt = Instant.now();
|
||||
}
|
||||
|
||||
// Getters and Setters
|
||||
|
||||
public UUID getClubId() { return clubId; }
|
||||
public void setClubId(UUID clubId) { this.clubId = clubId; }
|
||||
|
||||
public String getStripeCustomerId() { return stripeCustomerId; }
|
||||
public void setStripeCustomerId(String stripeCustomerId) { this.stripeCustomerId = stripeCustomerId; }
|
||||
|
||||
public String getStripeSubscriptionId() { return stripeSubscriptionId; }
|
||||
public void setStripeSubscriptionId(String stripeSubscriptionId) { this.stripeSubscriptionId = stripeSubscriptionId; }
|
||||
|
||||
public PlanTier getPlanTier() { return planTier; }
|
||||
public void setPlanTier(PlanTier planTier) { this.planTier = planTier; }
|
||||
|
||||
public Integer getMemberLimit() { return memberLimit; }
|
||||
public void setMemberLimit(Integer memberLimit) { this.memberLimit = memberLimit; }
|
||||
|
||||
public Instant getTrialEndsAt() { return trialEndsAt; }
|
||||
public void setTrialEndsAt(Instant trialEndsAt) { this.trialEndsAt = trialEndsAt; }
|
||||
|
||||
public Instant getCurrentPeriodStart() { return currentPeriodStart; }
|
||||
public void setCurrentPeriodStart(Instant currentPeriodStart) { this.currentPeriodStart = currentPeriodStart; }
|
||||
|
||||
public Instant getCurrentPeriodEnd() { return currentPeriodEnd; }
|
||||
public void setCurrentPeriodEnd(Instant currentPeriodEnd) { this.currentPeriodEnd = currentPeriodEnd; }
|
||||
|
||||
public SubscriptionStatus getStatus() { return status; }
|
||||
public void setStatus(SubscriptionStatus status) { this.status = status; }
|
||||
|
||||
public Instant getCanceledAt() { return canceledAt; }
|
||||
public void setCanceledAt(Instant canceledAt) { this.canceledAt = canceledAt; }
|
||||
|
||||
public Instant getUpdatedAt() { return updatedAt; }
|
||||
public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
public enum PlanTier {
|
||||
TRIAL,
|
||||
STARTER,
|
||||
PRO,
|
||||
ENTERPRISE
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
public enum SubscriptionStatus {
|
||||
TRIALING,
|
||||
ACTIVE,
|
||||
PAST_DUE,
|
||||
CANCELED,
|
||||
UNPAID
|
||||
}
|
||||
Reference in New Issue
Block a user