Phase 3.7: paid pairing flow + returning chat + extension flip
- Backend: payment_sessions + pairing_failures tables; payment.service.js and pairing-failure.service.js (new); rewritten pairing.service.js (payment-gated blast + targeted "Curhat lagi" + cancel + fallback); rewritten extension.service.js (data-driven auto-approve with offline safeguard, charge-at-approval); pricing.service.js (extension tiers without free trial); mitra-status.service.js (countAvailableMitras cached path); 60s sweeper for stale payment sessions - Backend routes: client.payment.routes, client.mitra-availability.routes, internal/failed-pairings.routes; client.chat.routes rewritten for payment-gated start + /returning + /cancel + /fallback-to-blast; internal/config.routes adds 4 new keys with Valkey invalidate publish - client_app: mitra-availability poll, payment screen + notifier, pairing notifier rewrite (PairingTargetedWaiting + PairingFailed states), targeted-waiting overlay + bestie-unavailable dialog, "Curhat lagi" CTA, failed-pairing terminal, extension via payment-session - mitra_app: PairingRequestType enum, returning-chat 20s countdown auto-dismiss, extension card "otomatis disetujui" copy - control_center: 4 new config rows in Settings, Failed Pairings page (filter + paginate + action menu), sidebar + route registered - Test infrastructure: Vitest backend (7/7 pass), Playwright CC (4/4 pass), Maestro mobile scaffold (CLI install pending) - Bugs found via Playwright + fixed: LoginPage labels not associated with inputs (a11y); backend internal CORS missing PATCH/PUT/DELETE in allow-methods (silent settings breakage in browsers since Stage 4) - Docs: phase3.7.md PRD, phase3.7-plan.md, phase3.7-questions.md (Q&A), phase3.7-testing.md (E2E checklist), phase3.7-test-run-2026-05-03.md (today's run results) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,7 @@ part of 'chat_notifier.dart';
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$chatHash() => r'30e0aa41d2022e5bb080877e23fcb0a0c0c4b927';
|
||||
String _$chatHash() => r'35388d2977db9cee4307697524620b22691c54f5';
|
||||
|
||||
/// See also [Chat].
|
||||
@ProviderFor(Chat)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import '../api/api_client_provider.dart';
|
||||
import 'active_session_notifier.dart';
|
||||
@@ -41,14 +40,42 @@ class SessionClosure extends _$SessionClosure {
|
||||
@override
|
||||
SessionClosureData build() => const ClosureInitialData();
|
||||
|
||||
/// Extension request is a 3-step flow with the extension cost held in its
|
||||
/// own `payment_session` (never combined with a free trial). Server-side,
|
||||
/// the extension service refuses requests without an
|
||||
/// `extension_payment_session_id` on a confirmed, is_extension payment session.
|
||||
///
|
||||
/// 1. POST `/api/client/payment-sessions` with `is_extension: true`
|
||||
/// 2. POST `/api/client/payment-sessions/:id/confirm`
|
||||
/// 3. POST `/api/client/chat/session/:sessionId/extend` with the
|
||||
/// extension_payment_session_id from step 2.
|
||||
///
|
||||
/// Charge timing is server-side: only on actual approve / auto-approve.
|
||||
/// If the mitra explicitly rejects within 10s the payment is failed back, no charge.
|
||||
Future<void> requestExtension(String sessionId, {required int durationMinutes, required int price}) async {
|
||||
state = const ExtendingWaitingMitraData();
|
||||
try {
|
||||
await ref.read(apiClientProvider).post('/api/client/chat/session/$sessionId/extend', data: {
|
||||
final api = ref.read(apiClientProvider);
|
||||
|
||||
final createResp = await api.post('/api/client/payment-sessions/', data: {
|
||||
'duration_minutes': durationMinutes,
|
||||
'is_extension': true,
|
||||
});
|
||||
final paymentSessionId = (createResp['data'] as Map<String, dynamic>)['id'] as String;
|
||||
|
||||
// Backend rejects truly empty bodies on confirm, so always send `{}`.
|
||||
await api.post(
|
||||
'/api/client/payment-sessions/$paymentSessionId/confirm',
|
||||
data: const <String, dynamic>{},
|
||||
);
|
||||
|
||||
// Trigger the extension request. The actual approve/reject round-trip is
|
||||
// owned by the chat WS — ChatNotifier surfaces it.
|
||||
await api.post('/api/client/chat/session/$sessionId/extend', data: {
|
||||
'duration_minutes': durationMinutes,
|
||||
'price': price,
|
||||
'extension_payment_session_id': paymentSessionId,
|
||||
});
|
||||
// Response will come via WebSocket (ChatBloc/ChatNotifier handles it)
|
||||
} catch (e) {
|
||||
state = const ClosureErrorData('Gagal meminta perpanjangan.');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user