feat: add manual subscription finalization functionality and support for expired plan notifications

This commit is contained in:
2026-07-19 13:20:33 -03:00
parent 78ee86ac48
commit af6c87acb6
20 changed files with 462 additions and 39 deletions
+17
View File
@@ -109,6 +109,23 @@ export const extendUserSubscription = async (data: ExtendUserSubscriptionPayload
return res.data;
};
export type FinalizeUserSubscriptionPayload = {
userId: string;
reason?: string;
};
export type FinalizeUserSubscriptionResult = {
userId: string;
subscriptionId: string;
previousEndDate: string;
newEndDate: string;
};
export const finalizeUserSubscription = async (data: FinalizeUserSubscriptionPayload): Promise<FinalizeUserSubscriptionResult> => {
const res = await sysadminApi.post('/users/finalize-subscription', data);
return res.data;
};
export type Plan = {
id?: string;
_id?: string;
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { motion } from 'framer-motion';
import { CreditCard, X } from 'lucide-react';
import { extendUserSubscription, getUserSubscriptionDetails, type UserSubscriptionDetails } from '../../api/sysadmin';
import { extendUserSubscription, finalizeUserSubscription, getUserSubscriptionDetails, type UserSubscriptionDetails } from '../../api/sysadmin';
import { formatDateTime } from '../../utils/formatters';
type Props = {
@@ -17,6 +17,7 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
const [extensionLoading, setExtensionLoading] = useState(false);
const [extensionMessage, setExtensionMessage] = useState('');
const [extensionError, setExtensionError] = useState('');
const [finalizeLoading, setFinalizeLoading] = useState(false);
const formatAmount = (value?: number) => {
if (typeof value !== 'number') return '-';
@@ -61,6 +62,29 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
}
};
const handleFinalize = async () => {
if (!details?.currentSubscription) return;
const confirmed = window.confirm('Esto establecerá el vencimiento de la suscripción a ayer para forzar el comportamiento de plan vencido. No elimina datos ni cambia el plan directamente. ¿Continuar?');
if (!confirmed) return;
setFinalizeLoading(true);
setExtensionError('');
setExtensionMessage('');
try {
const result = await finalizeUserSubscription({
userId: user._id,
reason: 'Finalización manual desde sysadmin-cli para probar expiración de plan',
});
setExtensionMessage(`Período finalizado. El vencimiento quedó en ${formatDateTime(result.newEndDate)} para forzar la expiración en la próxima lectura.`);
await loadDetails();
} catch (e: any) {
setExtensionError(e.response?.data?.message || e.message || 'Error finalizando período');
} finally {
setFinalizeLoading(false);
}
};
const subscription = details?.currentSubscription;
return (
@@ -118,10 +142,10 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
Extiende la vigencia sin pago y registra un pago de bonificación por $0 asociado a esta suscripción.
</p>
<div className="extension-actions">
<button className="btn btn-secondary" disabled={extensionLoading} onClick={() => handleExtend({ mode: 'add_months', months: 1 })}>
<button className="btn btn-secondary" disabled={extensionLoading || finalizeLoading} onClick={() => handleExtend({ mode: 'add_months', months: 1 })}>
Extender 1 mes
</button>
<button className="btn btn-secondary" disabled={extensionLoading} onClick={() => handleExtend({ mode: 'add_months', months: 2 })}>
<button className="btn btn-secondary" disabled={extensionLoading || finalizeLoading} onClick={() => handleExtend({ mode: 'add_months', months: 2 })}>
Extender 2 meses
</button>
<div className="extension-date-action">
@@ -129,18 +153,27 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
type="date"
value={extensionDate}
onChange={(event) => setExtensionDate(event.target.value)}
disabled={extensionLoading}
disabled={extensionLoading || finalizeLoading}
/>
<button
className="btn btn-primary"
disabled={extensionLoading || !extensionDate}
disabled={extensionLoading || finalizeLoading || !extensionDate}
onClick={() => handleExtend({ mode: 'set_end_date', endDate: extensionDate })}
>
Establecer vencimiento
</button>
</div>
</div>
{extensionLoading && <p style={{ color: 'var(--text-muted)', marginTop: '0.75rem' }}>Aplicando ajuste...</p>}
<div className="finalize-action">
<div>
<h4>Finalizar período</h4>
<p>Establece el vencimiento en ayer para forzar el comportamiento de suscripción vencida en la próxima lectura. No elimina datos ni cambia el plan directamente.</p>
</div>
<button className="btn btn-danger" disabled={extensionLoading || finalizeLoading} onClick={handleFinalize}>
Finalizar período
</button>
</div>
{(extensionLoading || finalizeLoading) && <p style={{ color: 'var(--text-muted)', marginTop: '0.75rem' }}>Aplicando ajuste...</p>}
{extensionMessage && <p style={{ color: 'var(--success)', marginTop: '0.75rem' }}>{extensionMessage}</p>}
{extensionError && <p style={{ color: 'var(--danger)', marginTop: '0.75rem' }}>{extensionError}</p>}
</section>
@@ -226,6 +259,30 @@ export const SubscriptionDetailsModal = ({ user, onClose }: Props) => {
color: var(--text-main);
padding: 0.65rem 0.75rem;
}
.finalize-action {
display: flex;
justify-content: space-between;
gap: 1rem;
align-items: center;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid var(--glass-border);
}
.finalize-action h4 {
color: var(--danger);
font-size: 1rem;
margin-bottom: 0.35rem;
}
.finalize-action p {
color: var(--text-muted);
margin: 0;
}
@media (max-width: 640px) {
.finalize-action {
align-items: stretch;
flex-direction: column;
}
}
`}} />
</motion.div>
</div>