handling changes to stripe account where owner needs to provide information
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
"use strict";
|
||||
|
||||
module.exports = {
|
||||
up: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.addColumn("Users", "stripeRequirementsCurrentlyDue", {
|
||||
type: Sequelize.JSON,
|
||||
defaultValue: [],
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn("Users", "stripeRequirementsPastDue", {
|
||||
type: Sequelize.JSON,
|
||||
defaultValue: [],
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn("Users", "stripeDisabledReason", {
|
||||
type: Sequelize.STRING,
|
||||
allowNull: true,
|
||||
});
|
||||
|
||||
await queryInterface.addColumn("Users", "stripeRequirementsLastUpdated", {
|
||||
type: Sequelize.DATE,
|
||||
allowNull: true,
|
||||
});
|
||||
},
|
||||
|
||||
down: async (queryInterface, Sequelize) => {
|
||||
await queryInterface.removeColumn("Users", "stripeRequirementsCurrentlyDue");
|
||||
await queryInterface.removeColumn("Users", "stripeRequirementsPastDue");
|
||||
await queryInterface.removeColumn("Users", "stripeDisabledReason");
|
||||
await queryInterface.removeColumn("Users", "stripeRequirementsLastUpdated");
|
||||
},
|
||||
};
|
||||
@@ -124,6 +124,24 @@ const User = sequelize.define(
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
stripeRequirementsCurrentlyDue: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
allowNull: true,
|
||||
},
|
||||
stripeRequirementsPastDue: {
|
||||
type: DataTypes.JSON,
|
||||
defaultValue: [],
|
||||
allowNull: true,
|
||||
},
|
||||
stripeDisabledReason: {
|
||||
type: DataTypes.STRING,
|
||||
allowNull: true,
|
||||
},
|
||||
stripeRequirementsLastUpdated: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
loginAttempts: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
|
||||
@@ -2,6 +2,8 @@ const express = require("express");
|
||||
const { authenticateToken, requireVerifiedEmail } = require("../middleware/auth");
|
||||
const { User, Item } = require("../models");
|
||||
const StripeService = require("../services/stripeService");
|
||||
const StripeWebhookService = require("../services/stripeWebhookService");
|
||||
const emailServices = require("../services/email");
|
||||
const logger = require("../utils/logger");
|
||||
const router = express.Router();
|
||||
|
||||
@@ -168,7 +170,7 @@ router.post("/account-sessions", authenticateToken, requireVerifiedEmail, async
|
||||
}
|
||||
});
|
||||
|
||||
// Get account status
|
||||
// Get account status with reconciliation
|
||||
router.get("/account-status", authenticateToken, async (req, res, next) => {
|
||||
let user = null;
|
||||
try {
|
||||
@@ -190,6 +192,64 @@ router.get("/account-status", authenticateToken, async (req, res, next) => {
|
||||
payoutsEnabled: accountStatus.payouts_enabled,
|
||||
});
|
||||
|
||||
// Reconciliation: Compare fetched status with stored User fields
|
||||
const previousPayoutsEnabled = user.stripePayoutsEnabled;
|
||||
const currentPayoutsEnabled = accountStatus.payouts_enabled;
|
||||
const requirements = accountStatus.requirements || {};
|
||||
|
||||
// Check if status has changed and needs updating
|
||||
const statusChanged =
|
||||
previousPayoutsEnabled !== currentPayoutsEnabled ||
|
||||
JSON.stringify(user.stripeRequirementsCurrentlyDue || []) !==
|
||||
JSON.stringify(requirements.currently_due || []);
|
||||
|
||||
if (statusChanged) {
|
||||
reqLogger.info("Reconciling account status from API call", {
|
||||
userId: req.user.id,
|
||||
previousPayoutsEnabled,
|
||||
currentPayoutsEnabled,
|
||||
previousCurrentlyDue: user.stripeRequirementsCurrentlyDue?.length || 0,
|
||||
newCurrentlyDue: requirements.currently_due?.length || 0,
|
||||
});
|
||||
|
||||
// Update user with current status
|
||||
await user.update({
|
||||
stripePayoutsEnabled: currentPayoutsEnabled,
|
||||
stripeRequirementsCurrentlyDue: requirements.currently_due || [],
|
||||
stripeRequirementsPastDue: requirements.past_due || [],
|
||||
stripeDisabledReason: requirements.disabled_reason || null,
|
||||
stripeRequirementsLastUpdated: new Date(),
|
||||
});
|
||||
|
||||
// If payouts just became disabled (true -> false), send notification
|
||||
if (!currentPayoutsEnabled && previousPayoutsEnabled) {
|
||||
reqLogger.warn("Payouts disabled detected during reconciliation", {
|
||||
userId: req.user.id,
|
||||
disabledReason: requirements.disabled_reason,
|
||||
});
|
||||
|
||||
try {
|
||||
const disabledReason = StripeWebhookService.formatDisabledReason(
|
||||
requirements.disabled_reason
|
||||
);
|
||||
|
||||
await emailServices.payment.sendPayoutsDisabledEmail(user.email, {
|
||||
ownerName: user.firstName || user.name,
|
||||
disabledReason,
|
||||
});
|
||||
|
||||
reqLogger.info("Sent payouts disabled email during reconciliation", {
|
||||
userId: req.user.id,
|
||||
});
|
||||
} catch (emailError) {
|
||||
reqLogger.error("Failed to send payouts disabled email", {
|
||||
userId: req.user.id,
|
||||
error: emailError.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json({
|
||||
accountId: accountStatus.id,
|
||||
detailsSubmitted: accountStatus.details_submitted,
|
||||
|
||||
@@ -213,6 +213,46 @@ class PaymentEmailService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send notification when owner's payouts are disabled due to requirements
|
||||
* @param {string} ownerEmail - Owner's email address
|
||||
* @param {Object} params - Email parameters
|
||||
* @param {string} params.ownerName - Owner's name
|
||||
* @param {string} params.disabledReason - Human-readable reason for disabling
|
||||
* @returns {Promise<{success: boolean, messageId?: string, error?: string}>}
|
||||
*/
|
||||
async sendPayoutsDisabledEmail(ownerEmail, params) {
|
||||
if (!this.initialized) {
|
||||
await this.initialize();
|
||||
}
|
||||
|
||||
try {
|
||||
const { ownerName, disabledReason } = params;
|
||||
|
||||
const variables = {
|
||||
ownerName: ownerName || "there",
|
||||
disabledReason:
|
||||
disabledReason ||
|
||||
"Additional verification is required for your account.",
|
||||
earningsUrl: `${process.env.FRONTEND_URL}/earnings`,
|
||||
};
|
||||
|
||||
const htmlContent = await this.templateManager.renderTemplate(
|
||||
"payoutsDisabledToOwner",
|
||||
variables
|
||||
);
|
||||
|
||||
return await this.emailClient.sendEmail(
|
||||
ownerEmail,
|
||||
"Action Required: Your payouts have been paused - Village Share",
|
||||
htmlContent
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to send payouts disabled email:", error);
|
||||
return { success: false, error: error.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send dispute alert to platform admin
|
||||
* Called when a new dispute is opened
|
||||
|
||||
@@ -16,19 +16,23 @@ class StripeWebhookService {
|
||||
|
||||
/**
|
||||
* Handle account.updated webhook event.
|
||||
* Triggers payouts for owner when payouts_enabled becomes true.
|
||||
* Tracks requirements, triggers payouts when enabled, and notifies when disabled.
|
||||
* @param {Object} account - The Stripe account object from the webhook
|
||||
* @returns {Object} - { processed, payoutsTriggered, payoutResults }
|
||||
* @returns {Object} - { processed, payoutsTriggered, payoutResults, notificationSent }
|
||||
*/
|
||||
static async handleAccountUpdated(account) {
|
||||
const accountId = account.id;
|
||||
const payoutsEnabled = account.payouts_enabled;
|
||||
const requirements = account.requirements || {};
|
||||
|
||||
logger.info("Processing account.updated webhook", {
|
||||
accountId,
|
||||
payoutsEnabled,
|
||||
chargesEnabled: account.charges_enabled,
|
||||
detailsSubmitted: account.details_submitted,
|
||||
currentlyDue: requirements.currently_due?.length || 0,
|
||||
pastDue: requirements.past_due?.length || 0,
|
||||
disabledReason: requirements.disabled_reason,
|
||||
});
|
||||
|
||||
// Find user with this Stripe account
|
||||
@@ -41,18 +45,33 @@ class StripeWebhookService {
|
||||
return { processed: false, reason: "user_not_found" };
|
||||
}
|
||||
|
||||
// Store previous state before update
|
||||
const previousPayoutsEnabled = user.stripePayoutsEnabled;
|
||||
|
||||
// Update user's payouts_enabled status
|
||||
await user.update({ stripePayoutsEnabled: payoutsEnabled });
|
||||
// Update user with all account status fields
|
||||
await user.update({
|
||||
stripePayoutsEnabled: payoutsEnabled,
|
||||
stripeRequirementsCurrentlyDue: requirements.currently_due || [],
|
||||
stripeRequirementsPastDue: requirements.past_due || [],
|
||||
stripeDisabledReason: requirements.disabled_reason || null,
|
||||
stripeRequirementsLastUpdated: new Date(),
|
||||
});
|
||||
|
||||
logger.info("Updated user stripePayoutsEnabled", {
|
||||
logger.info("Updated user Stripe account status", {
|
||||
userId: user.id,
|
||||
accountId,
|
||||
previousPayoutsEnabled,
|
||||
newPayoutsEnabled: payoutsEnabled,
|
||||
currentlyDue: requirements.currently_due?.length || 0,
|
||||
pastDue: requirements.past_due?.length || 0,
|
||||
});
|
||||
|
||||
const result = {
|
||||
processed: true,
|
||||
payoutsTriggered: false,
|
||||
notificationSent: false,
|
||||
};
|
||||
|
||||
// If payouts just became enabled (false -> true), process pending payouts
|
||||
if (payoutsEnabled && !previousPayoutsEnabled) {
|
||||
logger.info("Payouts enabled for user, processing pending payouts", {
|
||||
@@ -60,15 +79,69 @@ class StripeWebhookService {
|
||||
accountId,
|
||||
});
|
||||
|
||||
const result = await this.processPayoutsForOwner(user.id);
|
||||
return {
|
||||
processed: true,
|
||||
payoutsTriggered: true,
|
||||
payoutResults: result,
|
||||
};
|
||||
result.payoutsTriggered = true;
|
||||
result.payoutResults = await this.processPayoutsForOwner(user.id);
|
||||
}
|
||||
|
||||
return { processed: true, payoutsTriggered: false };
|
||||
// If payouts just became disabled (true -> false), notify the owner
|
||||
if (!payoutsEnabled && previousPayoutsEnabled) {
|
||||
logger.warn("Payouts disabled for user", {
|
||||
userId: user.id,
|
||||
accountId,
|
||||
disabledReason: requirements.disabled_reason,
|
||||
currentlyDue: requirements.currently_due,
|
||||
});
|
||||
|
||||
try {
|
||||
const disabledReason = this.formatDisabledReason(requirements.disabled_reason);
|
||||
|
||||
await emailServices.payment.sendPayoutsDisabledEmail(user.email, {
|
||||
ownerName: user.firstName || user.name,
|
||||
disabledReason,
|
||||
});
|
||||
|
||||
result.notificationSent = true;
|
||||
|
||||
logger.info("Sent payouts disabled notification to owner", {
|
||||
userId: user.id,
|
||||
accountId,
|
||||
disabledReason: requirements.disabled_reason,
|
||||
});
|
||||
} catch (emailError) {
|
||||
logger.error("Failed to send payouts disabled notification", {
|
||||
userId: user.id,
|
||||
accountId,
|
||||
error: emailError.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Stripe disabled_reason code to user-friendly message.
|
||||
* @param {string} reason - Stripe disabled_reason code
|
||||
* @returns {string} User-friendly message
|
||||
*/
|
||||
static formatDisabledReason(reason) {
|
||||
const reasonMap = {
|
||||
"requirements.past_due":
|
||||
"Some required information is past due and must be provided to continue receiving payouts.",
|
||||
"requirements.pending_verification":
|
||||
"Your submitted information is being verified. This usually takes a few minutes.",
|
||||
listed: "Your account has been listed for review due to potential policy concerns.",
|
||||
platform_paused:
|
||||
"Payouts have been temporarily paused by the platform.",
|
||||
rejected_fraud: "Your account was flagged for potential fraudulent activity.",
|
||||
rejected_listed: "Your account has been rejected due to policy concerns.",
|
||||
rejected_terms_of_service:
|
||||
"Your account was rejected due to a terms of service violation.",
|
||||
rejected_other: "Your account was rejected. Please contact support for more information.",
|
||||
under_review: "Your account is under review. We'll notify you when the review is complete.",
|
||||
};
|
||||
|
||||
return reasonMap[reason] || "Additional verification is required for your account.";
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -444,6 +517,10 @@ class StripeWebhookService {
|
||||
await user.update({
|
||||
stripeConnectedAccountId: null,
|
||||
stripePayoutsEnabled: false,
|
||||
stripeRequirementsCurrentlyDue: [],
|
||||
stripeRequirementsPastDue: [],
|
||||
stripeDisabledReason: null,
|
||||
stripeRequirementsLastUpdated: null,
|
||||
});
|
||||
|
||||
logger.info("Cleared Stripe connection for deauthorized account", {
|
||||
|
||||
311
backend/templates/emails/payoutsDisabledToOwner.html
Normal file
311
backend/templates/emails/payoutsDisabledToOwner.html
Normal file
@@ -0,0 +1,311 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>Action Required - Village Share</title>
|
||||
<style>
|
||||
/* Reset styles */
|
||||
body,
|
||||
table,
|
||||
td,
|
||||
p,
|
||||
a,
|
||||
li,
|
||||
blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table,
|
||||
td {
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
}
|
||||
img {
|
||||
-ms-interpolation-mode: bicubic;
|
||||
}
|
||||
|
||||
/* Base styles */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100% !important;
|
||||
min-width: 100%;
|
||||
height: 100%;
|
||||
background-color: #f8f9fa;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.email-container {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
background-color: #ffffff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
|
||||
padding: 40px 30px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #212529;
|
||||
text-decoration: none;
|
||||
letter-spacing: -1px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
color: #495057;
|
||||
font-size: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* Content */
|
||||
.content {
|
||||
padding: 40px 30px;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 20px 0;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.content h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 30px 0 15px 0;
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
.content p {
|
||||
margin: 0 0 16px 0;
|
||||
color: #6c757d;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.content strong {
|
||||
color: #495057;
|
||||
}
|
||||
|
||||
/* Alert box */
|
||||
.alert-box {
|
||||
background-color: #fff3cd;
|
||||
border-left: 4px solid #ffc107;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.alert-box p {
|
||||
margin: 0 0 10px 0;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.alert-box p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Warning display */
|
||||
.warning-display {
|
||||
background: linear-gradient(135deg, #ffc107 0%, #e0a800 100%);
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
margin: 30px 0;
|
||||
box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.warning-label {
|
||||
color: #212529;
|
||||
font-size: 14px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
color: #212529;
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.warning-subtitle {
|
||||
color: #212529;
|
||||
font-size: 14px;
|
||||
margin-top: 10px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* Button */
|
||||
.button {
|
||||
display: inline-block;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #ffffff !important;
|
||||
text-decoration: none;
|
||||
padding: 16px 32px;
|
||||
border-radius: 6px;
|
||||
font-weight: 600;
|
||||
margin: 20px 0;
|
||||
text-align: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
/* Info box */
|
||||
.info-box {
|
||||
background-color: #e7f3ff;
|
||||
border-left: 4px solid #0066cc;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.info-box p {
|
||||
margin: 0 0 10px 0;
|
||||
color: #004085;
|
||||
}
|
||||
|
||||
.info-box p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.footer {
|
||||
background-color: #f8f9fa;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-top: 1px solid #e9ecef;
|
||||
}
|
||||
|
||||
.footer p {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.footer a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media only screen and (max-width: 600px) {
|
||||
.email-container {
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.header,
|
||||
.content,
|
||||
.footer {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.logo {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.content h1 {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.warning-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="email-container">
|
||||
<div class="header">
|
||||
<div class="logo">Village Share</div>
|
||||
<div class="tagline">Action Required</div>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<p>Hi {{ownerName}},</p>
|
||||
|
||||
<p>
|
||||
Your payouts have been temporarily paused because additional
|
||||
verification is needed for your account.
|
||||
</p>
|
||||
|
||||
<div class="warning-display">
|
||||
<div class="warning-label">Account Status</div>
|
||||
<div class="warning-title">Payouts Paused</div>
|
||||
<div class="warning-subtitle">Complete verification to resume</div>
|
||||
</div>
|
||||
|
||||
<div class="alert-box">
|
||||
<p><strong>What happened:</strong></p>
|
||||
<p>
|
||||
{{disabledReason}}
|
||||
</p>
|
||||
<p><strong>What to do:</strong></p>
|
||||
<p>
|
||||
Visit your Earnings page to complete the required verification
|
||||
steps. This usually only takes a few minutes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center">
|
||||
<a href="{{earningsUrl}}" class="button">Go to Earnings</a>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>What happens next?</strong></p>
|
||||
<p>
|
||||
Once you complete the verification, your payouts will resume
|
||||
automatically. Any pending earnings will be deposited to your bank
|
||||
account on the normal schedule.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
We apologize for any inconvenience. Your earnings are safe and will be
|
||||
deposited as soon as verification is complete.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p><strong>Village Share</strong></p>
|
||||
<p>
|
||||
This is an important notification about your account. You received
|
||||
this message because your payouts have been paused pending
|
||||
verification.
|
||||
</p>
|
||||
<p>If you have any questions, please contact our support team.</p>
|
||||
<p>© 2025 Village Share. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -3,12 +3,14 @@ import React from "react";
|
||||
interface EarningsStatusProps {
|
||||
hasStripeAccount: boolean;
|
||||
isOnboardingComplete?: boolean;
|
||||
payoutsEnabled?: boolean;
|
||||
onSetupClick: () => void;
|
||||
}
|
||||
|
||||
const EarningsStatus: React.FC<EarningsStatusProps> = ({
|
||||
hasStripeAccount,
|
||||
isOnboardingComplete = false,
|
||||
payoutsEnabled = true,
|
||||
onSetupClick,
|
||||
}) => {
|
||||
// No Stripe account exists
|
||||
@@ -55,7 +57,29 @@ const EarningsStatus: React.FC<EarningsStatusProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// Account exists and is fully set up
|
||||
// Account exists, onboarding complete, but payouts disabled
|
||||
if (!payoutsEnabled) {
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="mb-3">
|
||||
<i
|
||||
className="bi bi-exclamation-triangle text-danger"
|
||||
style={{ fontSize: "2.5rem" }}
|
||||
></i>
|
||||
</div>
|
||||
<h6 className="text-danger">Action Required</h6>
|
||||
<p className="text-muted small mb-3">
|
||||
Additional verification is needed to continue receiving payouts.
|
||||
Please complete the required steps to resume your earnings.
|
||||
</p>
|
||||
<button className="btn btn-danger" onClick={onSetupClick}>
|
||||
Complete Verification
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Account exists and is fully set up with payouts enabled
|
||||
return (
|
||||
<div className="text-center">
|
||||
<div className="mb-3">
|
||||
|
||||
@@ -125,6 +125,11 @@ const EarningsDashboard: React.FC = () => {
|
||||
|
||||
const hasStripeAccount = !!userProfile?.stripeConnectedAccountId;
|
||||
const isOnboardingComplete = accountStatus?.detailsSubmitted ?? false;
|
||||
const payoutsEnabled = accountStatus?.payoutsEnabled ?? true;
|
||||
|
||||
// Show setup card if: no account, onboarding incomplete, or payouts disabled
|
||||
const showSetupCard =
|
||||
!hasStripeAccount || !isOnboardingComplete || !payoutsEnabled;
|
||||
|
||||
return (
|
||||
<div className="container mt-4">
|
||||
@@ -147,8 +152,8 @@ const EarningsDashboard: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Earnings Setup - only show if not fully set up */}
|
||||
{(!hasStripeAccount || !isOnboardingComplete) && (
|
||||
{/* Earnings Setup - show if not fully set up or payouts disabled */}
|
||||
{showSetupCard && (
|
||||
<div className="card mb-4">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">Earnings Setup</h5>
|
||||
@@ -157,6 +162,7 @@ const EarningsDashboard: React.FC = () => {
|
||||
<EarningsStatus
|
||||
hasStripeAccount={hasStripeAccount}
|
||||
isOnboardingComplete={isOnboardingComplete}
|
||||
payoutsEnabled={payoutsEnabled}
|
||||
onSetupClick={() => setShowOnboarding(true)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user