acceptNotification method

Future<DVPurchaseResult> acceptNotification(
  1. DVContext context,
  2. DVStore store, {
  3. required String body,
  4. required Map<String, String> headers,
})

Applies a store server notification delivered to store's endpoint.

body and headers are the request's. Throws DVPurchaseRefused for a signature that does not verify, having changed nothing; the endpoint answers that with a client error. Every other outcome is an acknowledgement, including a replay, a stale answer and an undeclared product (DV-PURCHASE-004), because a store retries what is not acknowledged. A failure part-way -- an acknowledgement the store did not take -- rolls the grant and the notification's claim back and rethrows, so the store's retry is applied rather than mistaken for a replay.

Implementation

Future<DVPurchaseResult> acceptNotification(
  DVContext context,
  DVStore store, {
  required String body,
  required Map<String, String> headers,
}) async {
  final DVStoreAdapter adapter = _adapter(store);
  final DVStoreNotification notification;
  try {
    notification = await adapter.verifyNotification(body, headers);
  } on DVStoreNothingToApply {
    return DVPurchaseResult._ignored;
  } on DVStoreRefusal catch (refusal) {
    _log.log(
      'A ${store.name} notification was refused: ${refusal.reason}',
      level: DVLogLevel.warn,
      context: <String, Object?>{'store': store.name},
    );
    throw DVPurchaseRefused(refusal.reason);
  }
  final DVStoreTransaction transaction = notification.transaction;
  if (transaction.store != store) {
    throw const DVPurchaseRefused(
        'the notification describes a purchase from a different store');
  }

  return _transactions<DVPurchaseResult>((DVContext unit) async {
    if (!await ledger.claimNotification(store, notification.notificationId)) {
      return DVPurchaseResult._replayed;
    }
    unit.compensate(
        () => ledger.releaseNotification(store, notification.notificationId));

    final DVPurchaseProduct? product =
        _byStoreId[(store, transaction.storeProductId)];
    if (product == null) {
      _undeclared(store, transaction);
      return DVPurchaseResult._undeclared;
    }

    final DVPurchaseGrant? previous =
        await ledger.find(store, transaction.originalTransactionId);
    if (previous == null) return DVPurchaseResult._unattributed;
    if (notification.signedAt.isBefore(previous.lastEventAt)) {
      return DVPurchaseResult._stale;
    }

    return _write(
      unit,
      adapter: adapter,
      transaction: transaction,
      product: product,
      customerKey: previous.customerKey,
      previous: previous,
      eventAt: notification.signedAt,
      notificationId: notification.notificationId,
    );
  });
}