fix(compras): revocar el PRO tras un reembolso sin penalizar a quien pago
Verificacion silenciosa en segundo plano con queryPastPurchases, al abrir o volver a la app y al cargar Android Auto. Solo revoca tras dos respuestas validas de Play sin la compra separadas 12h; sin red o con error no toca nada. Reactiva el PRO automaticamente si Play confirma la compra.
This commit is contained in:
@@ -2,6 +2,8 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:in_app_purchase/in_app_purchase.dart';
|
||||
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
|
||||
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
|
||||
import 'package:pluriwave/servicios/servicio_compras.dart';
|
||||
|
||||
/// Port-boundary tests (Design ADR-2 "keeps Strict TDD viable with zero
|
||||
@@ -17,8 +19,12 @@ import 'package:pluriwave/servicios/servicio_compras.dart';
|
||||
class _InAppPurchaseFalso implements InAppPurchase {
|
||||
final _compras = StreamController<List<PurchaseDetails>>.broadcast();
|
||||
int restauracionesPedidas = 0;
|
||||
bool disponible = true;
|
||||
final completadas = <PurchaseDetails>[];
|
||||
|
||||
@override
|
||||
Future<bool> isAvailable() async => disponible;
|
||||
|
||||
@override
|
||||
Stream<List<PurchaseDetails>> get purchaseStream => _compras.stream;
|
||||
|
||||
@@ -52,6 +58,40 @@ PurchaseDetails _compraFalsa(PurchaseStatus status) => PurchaseDetails(
|
||||
status: status,
|
||||
);
|
||||
|
||||
/// A Play Billing past purchase as `queryPastPurchases` returns it.
|
||||
GooglePlayPurchaseDetails _compraPasada(
|
||||
PurchaseStatus status, {
|
||||
String productId = ServicioComprasPlayBilling.idProducto,
|
||||
}) => GooglePlayPurchaseDetails(
|
||||
purchaseID: 'GPA.1',
|
||||
productID: productId,
|
||||
verificationData: PurchaseVerificationData(
|
||||
localVerificationData: '{}',
|
||||
serverVerificationData: 'token',
|
||||
source: 'google_play',
|
||||
),
|
||||
transactionDate: '0',
|
||||
status: status,
|
||||
billingClientPurchase: PurchaseWrapper(
|
||||
orderId: 'GPA.1',
|
||||
packageName: 'es.freetimelab.pluriwave',
|
||||
purchaseTime: 0,
|
||||
purchaseToken: 'token',
|
||||
signature: 'firma',
|
||||
products: <String>[productId],
|
||||
isAutoRenewing: false,
|
||||
originalJson: '{}',
|
||||
isAcknowledged: true,
|
||||
purchaseState: PurchaseStateWrapper.purchased,
|
||||
),
|
||||
);
|
||||
|
||||
IAPError _errorBilling() => IAPError(
|
||||
source: 'google_play',
|
||||
code: 'restore_transactions_failed',
|
||||
message: 'BillingResponse.serviceUnavailable',
|
||||
);
|
||||
|
||||
void main() {
|
||||
group('eventoDesdeEstadoCompra', () {
|
||||
test('purchased -> comprada', () {
|
||||
@@ -137,8 +177,8 @@ void main() {
|
||||
final servicio = ServicioComprasPlayBilling(inAppPurchase: iap);
|
||||
addTearDown(servicio.dispose);
|
||||
|
||||
final compra =
|
||||
_compraFalsa(PurchaseStatus.purchased)..pendingCompletePurchase = true;
|
||||
final compra = _compraFalsa(PurchaseStatus.purchased)
|
||||
..pendingCompletePurchase = true;
|
||||
iap.emitir(<PurchaseDetails>[compra]);
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
@@ -146,6 +186,153 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('ServicioComprasPlayBilling.consultarPropiedad (verificacion '
|
||||
'silenciosa)', () {
|
||||
late _InAppPurchaseFalso iap;
|
||||
|
||||
setUp(() => iap = _InAppPurchaseFalso());
|
||||
tearDown(() => iap.dispose());
|
||||
|
||||
ServicioComprasPlayBilling servicioCon(
|
||||
Future<QueryPurchaseDetailsResponse> Function() consulta, {
|
||||
Duration limite = const Duration(seconds: 10),
|
||||
}) {
|
||||
final servicio = ServicioComprasPlayBilling(
|
||||
inAppPurchase: iap,
|
||||
consultarComprasPasadas: consulta,
|
||||
limiteConsultaPropiedad: limite,
|
||||
);
|
||||
addTearDown(servicio.dispose);
|
||||
return servicio;
|
||||
}
|
||||
|
||||
test('producto comprado -> poseida', () async {
|
||||
final servicio = servicioCon(
|
||||
() async => QueryPurchaseDetailsResponse(
|
||||
pastPurchases: [_compraPasada(PurchaseStatus.purchased)],
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
await servicio.consultarPropiedad(),
|
||||
ResultadoVerificacionLicencia.poseida,
|
||||
);
|
||||
});
|
||||
|
||||
test('respuesta correcta sin el producto -> noPoseida', () async {
|
||||
final servicio = servicioCon(
|
||||
() async => QueryPurchaseDetailsResponse(
|
||||
pastPurchases: [
|
||||
_compraPasada(PurchaseStatus.purchased, productId: 'otro'),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
await servicio.consultarPropiedad(),
|
||||
ResultadoVerificacionLicencia.noPoseida,
|
||||
);
|
||||
});
|
||||
|
||||
test('respuesta con error y sin producto -> desconocido', () async {
|
||||
final servicio = servicioCon(
|
||||
() async => QueryPurchaseDetailsResponse(
|
||||
pastPurchases: const [],
|
||||
error: _errorBilling(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
await servicio.consultarPropiedad(),
|
||||
ResultadoVerificacionLicencia.desconocido,
|
||||
);
|
||||
});
|
||||
|
||||
test(
|
||||
'respuesta con error parcial pero con el producto -> poseida',
|
||||
() async {
|
||||
final servicio = servicioCon(
|
||||
() async => QueryPurchaseDetailsResponse(
|
||||
pastPurchases: [_compraPasada(PurchaseStatus.purchased)],
|
||||
error: _errorBilling(),
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
await servicio.consultarPropiedad(),
|
||||
ResultadoVerificacionLicencia.poseida,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('compra pendiente -> desconocido (nunca revoca)', () async {
|
||||
final servicio = servicioCon(
|
||||
() async => QueryPurchaseDetailsResponse(
|
||||
pastPurchases: [_compraPasada(PurchaseStatus.pending)],
|
||||
),
|
||||
);
|
||||
|
||||
expect(
|
||||
await servicio.consultarPropiedad(),
|
||||
ResultadoVerificacionLicencia.desconocido,
|
||||
);
|
||||
});
|
||||
|
||||
test('billing no disponible -> desconocido, sin consultar', () async {
|
||||
var consultas = 0;
|
||||
iap.disponible = false;
|
||||
final servicio = servicioCon(() async {
|
||||
consultas++;
|
||||
return QueryPurchaseDetailsResponse(pastPurchases: const []);
|
||||
});
|
||||
|
||||
expect(
|
||||
await servicio.consultarPropiedad(),
|
||||
ResultadoVerificacionLicencia.desconocido,
|
||||
);
|
||||
expect(consultas, 0);
|
||||
});
|
||||
|
||||
test('excepcion -> desconocido', () async {
|
||||
final servicio = servicioCon(
|
||||
() async => throw Exception('BillingClient desconectado'),
|
||||
);
|
||||
|
||||
expect(
|
||||
await servicio.consultarPropiedad(),
|
||||
ResultadoVerificacionLicencia.desconocido,
|
||||
);
|
||||
});
|
||||
|
||||
test('timeout -> desconocido', () async {
|
||||
final servicio = servicioCon(
|
||||
() => Completer<QueryPurchaseDetailsResponse>().future,
|
||||
limite: const Duration(milliseconds: 10),
|
||||
);
|
||||
|
||||
expect(
|
||||
await servicio.consultarPropiedad(),
|
||||
ResultadoVerificacionLicencia.desconocido,
|
||||
);
|
||||
});
|
||||
|
||||
test('es silenciosa: no emite eventos de compra ni usa '
|
||||
'restorePurchases', () async {
|
||||
final servicio = servicioCon(
|
||||
() async => QueryPurchaseDetailsResponse(pastPurchases: const []),
|
||||
);
|
||||
final eventos = <EventoCompra>[];
|
||||
final sub = servicio.eventos.listen(eventos.add);
|
||||
addTearDown(sub.cancel);
|
||||
|
||||
await servicio.consultarPropiedad();
|
||||
await Future<void>.delayed(Duration.zero);
|
||||
|
||||
expect(eventos, isEmpty);
|
||||
expect(iap.restauracionesPedidas, 0);
|
||||
});
|
||||
});
|
||||
|
||||
test('idProducto es el identificador unico no-consumible', () {
|
||||
expect(ServicioComprasPlayBilling.idProducto, 'pluriwave_premium');
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user