feat(sprint-6): Phase 3 — Stripe integration (SEPA + PayPal + Card)
Deploy to Production / test (push) Has been cancelled
Deploy to Production / deploy (push) Has been cancelled

- 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:
Patrick Plate
2026-06-12 22:31:03 +02:00
parent 3232d2f7fd
commit 61e481b37b
17 changed files with 892 additions and 0 deletions
@@ -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
}