SaFi Bank Space : Contract Testing and PACT

What is Pact?

Pact is a code-first tool for testing HTTP and message integrations using contract tests. https://docs.pact.io/

What is contract test?

The responsibility of the contract test is to validate the provider APIs based on the given contract. A service that produces data can change over time, it’s important that the contracts with each service that consumes data from it continue to meet expectations. Contract tests provide a mechanism to explicitly verify that a provider meets a contract.

Three main parties in the contract tests are

Consumer: A client who wants to receive the data. An application or service that makes use of the functionality or data from another application to do its job.

Provider: An API server who provides data to clients. An application or service that provides functionality or data for other applications to use via an API.

Pact Broker(Pactflow): The Pact Broker is an application for sharing the contracts and verification results between the consumer and the provider.

Contract Testing Process (HTTP)

The process looks like this :

​​

  1. The consumer writes a unit test of its behaviour using a Mock provided by Pact

  2. Pact writes the interactions into a contract file (as a JSON document)

  3. The consumer publishes the contract to a broker (or shares the file in some other way)

  4. Pact retrieves the contracts and replays the requests against a locally running provider

  5. The provider should stub out its dependencies during a Pact test, to ensure tests are fast and more deterministic.

How to Write Contract Test

Consumer Pact Test

There are mainly 4 steps involved in consumer side for generating pact

  • Pact DSL is used to register request and response with mock server

  • Consumer tests fires a real request to the mock provider (created by pact framework)

  • Mock provider compares the expected and actual request and returns the response is comparison is successful.

  • The consumer tests confirms that the response is understood correctly.

Steps :

  1. Need to install the package first :

    # install pact_dart as a dev dependency
    flutter pub add --dev pact_dart
    
    # download and install the required libraries
    flutter pub run pact_dart:install
    
    # 🚀 now write some tests!

    or add manually to your package's pubspec.yaml :

    dev_dependencies:
      pact_dart: ^0.5.0
  2. Create/Write a test for XXXClient which interacts with external service. Example, we are testing our Cards API client.

  3. Setup the consumer and provider name pact = PactMockService(consumer-service-name, provider-service-name)

  4. In .given we give a text which is called a state, state is very important because using the same state provider will replay the request to verify later.

app/app_safi/contract_test/card_manager_contract_test.dart
// ignore_for_file: invalid_use_of_visible_for_testing_member

import 'package:data_abstraction/entity/auth/user_presence_check_args.dart';
import 'package:flutter_safi/common/http/digital_bank_client.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mobile_data/datasource/remote/card_remote_datasource.dart';
import 'package:mocktail/mocktail.dart';
import 'package:module_common/common/network/http_method_enum.dart';
import 'package:module_common/common/network/signing/request_signer.dart';
import 'package:module_common/common/wrappers/uuid_wrapper.dart';
import 'package:module_common/data/models/__mocks__/mock_card_model.dart';
import 'package:pact_dart/pact_dart.dart';

import '../test/__mocks__/mock_event_bus_cubit.dart';

void main() {
  late PactMockService pact;
  final uuid = UuidWrapper();
  final eventBusCubit = MockEventBusCubit();

  late MockRequestSigner requestSigner;
  late DigitalBankClient _client;
  late CardRemoteDatasource cardRemoteDatasource;

  // ignore: constant_identifier_names
  const CHARACTER_REGEX = r'^[a-zA-Z ]*$';
  const mockSafiCuid = 'ee6d61a7-7482-4c7a-a7db-29b774690375';
  const directory = 'contract_test';

  setUpAll(() {
    registerFallbackValue(SigningLevel.silentSigning);
    registerFallbackValue(const UserPresenceCheckArgs.ui());
  });

  setUp(() {
    pact = PactMockService(
      'safi-mobile-app',
      'card-manager',
    );

    requestSigner = MockRequestSigner();

    _client = DigitalBankClient.forContractTest(
      host: 'http://${pact.addr}',
      tykToken: 'token',
      uuid: uuid,
      requestSigner: requestSigner,
      eventBus: eventBusCubit,
    );

    cardRemoteDatasource = CardRemoteDatasource.forContractTest(
      _client,
      uuid,
    );
  });

  tearDown(() {
    reset(requestSigner);
    pact.reset();
  });

  test('GET : /v2/cards/{customerId}', () async {
    const customerId = 'ee6d61a7-7482-4c7a-a7db-29b774690375';
    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.get,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('GET : /v2/cards/{customerId}')
        .uponReceiving('A request to get cards by card id')
        .withRequest(
          'GET',
          '/v2/cards/$customerId',
        )
        .willRespondWith(
          200,
          body: PactMatchers.EachLike(
            [
              {
                'cardId':
                    PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
                'accountNo': PactMatchers.SomethingLike('0443482226'),
                'accountType': PactMatchers.SomethingLike('MAIN_ACCOUNT'),
                'customerId': PactMatchers.SomethingLike(
                    'ee6d61a7-7482-4c7a-a7db-29b774690375'),
                'cardType': PactMatchers.SomethingLike('DD1'),
                'cardPicture': PactMatchers.SomethingLike(
                    'https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png'),
                'namePrinted': PactMatchers.SomethingLike(false),
                'dynamicCVV': PactMatchers.SomethingLike(false),
                'internationalBlockingFlag': PactMatchers.SomethingLike(false),
                'embossName':
                    PactMatchers.Term(CHARACTER_REGEX, 'Maria Santos'),
                'maskingCardNo': PactMatchers.SomethingLike('0435411490164376'),
                'expiration': PactMatchers.SomethingLike('20270902'),
                'cardStatus': PactMatchers.SomethingLike('ACTIVE'),
                'orderingNumber': PactMatchers.IntegerLike(1),
                'key': PactMatchers.SomethingLike('TJD0ZsH'),
                'createdAt': null,
                'updatedAt': null,
                'cardDelivery': null,
                'cardLimit': null,
              },
            ],
          ),
        );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.getCards(customerId);

    expect(cards, hasLength(1));

    pact.writePactFile(directory: directory);
  });

  test('POST : /v2/cards/{cardId}/change-name', () async {
    const cardId = '08d8a7b9-5259-430d-8374-076592bfb4f5';
    const customerId = 'ee6d61a7-7482-4c7a-a7db-29b774690375';
    const updatedName = 'Maria Santos';
    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.post,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('POST : /v2/cards/{cardId}/change-name')
        .uponReceiving('A request to change card name')
        .withRequest('POST', '/v2/cards/$cardId/change-name', body: {
      'newCardName': updatedName
    }, headers: {
      'safi-cuid': '$customerId',
    }).willRespondWith(
      200,
      body: {
        'cardId': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
        'accountNo': PactMatchers.SomethingLike('0443482226'),
        'accountType': PactMatchers.SomethingLike('MAIN_ACCOUNT'),
        'customerId':
            PactMatchers.SomethingLike('ee6d61a7-7482-4c7a-a7db-29b774690375'),
        'cardType': PactMatchers.SomethingLike('DD1'),
        'cardPicture': PactMatchers.SomethingLike(
            'https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png'),
        'namePrinted': PactMatchers.SomethingLike(false),
        'dynamicCVV': PactMatchers.SomethingLike(false),
        'internationalBlockingFlag': PactMatchers.SomethingLike(false),
        'maskingCardNo': PactMatchers.SomethingLike('0435411490164376'),
        'expiration': PactMatchers.SomethingLike('20270902'),
        'cardStatus': PactMatchers.SomethingLike('ACTIVE'),
        'orderingNumber': PactMatchers.SomethingLike(1),
        'key': PactMatchers.SomethingLike('TJD0ZsH'),
        'createdAt': null,
        'updatedAt': null,
        'embossName': PactMatchers.Term(CHARACTER_REGEX, 'Maria Santos'),
        'cardDelivery': null,
        'cardLimit': null,
      },
    );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.changeCardName(
      cardId: cardId,
      customerId: customerId,
      updatedName: updatedName,
    );

    expect(
      cards.cardName,
      equals(updatedName),
      reason: 'Pact did respond with expected body',
    );

    pact.writePactFile(directory: directory);
  });

  test('POST - BlockCard : /v2/cards/{cardId}/card-lock', () async {
    const cardId = '08d8a7b9-5259-430d-8374-076592bfb4f5';
    const customerId = 'ee6d61a7-7482-4c7a-a7db-29b774690375';
    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.post,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('POST - BlockCard : /v2/cards/{cardId}/card-lock')
        .uponReceiving('A request to block card by card id')
        .withRequest('POST', '/v2/cards/$cardId/card-lock', body: {
      'function': 'BLOCK',
    }, headers: {
      'safi-cuid': '$customerId',
    }).willRespondWith(
      200,
      body: {
        'cardId': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
        'accountNo': PactMatchers.SomethingLike('0443482226'),
        'accountType': PactMatchers.SomethingLike('MAIN_ACCOUNT'),
        'customerId':
            PactMatchers.SomethingLike('ee6d61a7-7482-4c7a-a7db-29b774690375'),
        'cardType': PactMatchers.SomethingLike('DD1'),
        'cardPicture': PactMatchers.SomethingLike(
            'https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png'),
        'namePrinted': PactMatchers.SomethingLike(false),
        'dynamicCVV': PactMatchers.SomethingLike(false),
        'internationalBlockingFlag': PactMatchers.SomethingLike(false),
        'maskingCardNo': PactMatchers.SomethingLike('0435411490164376'),
        'expiration': PactMatchers.SomethingLike('20270902'),
        'cardStatus': PactMatchers.SomethingLike('PERMANENT_BLOCKED'),
        'orderingNumber': PactMatchers.SomethingLike(1),
        'key': PactMatchers.SomethingLike('TJD0ZsH'),
        'createdAt': null,
        'updatedAt': null,
        'embossName': PactMatchers.Term(CHARACTER_REGEX, 'Maria Santos'),
        'cardDelivery': null,
        'cardLimit': {
          'id': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
          'limitAmountCardTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveCardTransaction': PactMatchers.SomethingLike(true),
          'limitAmountAtmWithdrawal': PactMatchers.SomethingLike(10000.0),
          'isActiveAtmWithdrawal': PactMatchers.SomethingLike(true),
          'limitAmountOnlineTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveOnlineTransaction': PactMatchers.SomethingLike(true),
          'createdAt': null,
          'updatedAt': null
        },
      },
    );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.blockCard(
      cardId: cardId,
      customerId: customerId,
    );

    expect(
      cards.cardStatus.status,
      equals('PERMANENT_BLOCKED'),
      reason: 'Pact did respond with expected body',
    );

    pact.writePactFile(directory: directory);
  });

  test('POST - FreezeCard : /v2/cards/{cardId}/card-lock', () async {
    const cardId = '08d8a7b9-5259-430d-8374-076592bfb4f5';
    const customerId = 'ee6d61a7-7482-4c7a-a7db-29b774690375';
    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.post,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('POST - FreezeCard : /v2/cards/{cardId}/card-lock')
        .uponReceiving('A request to freeze card by card id')
        .withRequest('POST', '/v2/cards/$cardId/card-lock', body: {
      'function': 'FREEZE',
    }, headers: {
      'safi-cuid': '$customerId',
    }).willRespondWith(
      200,
      body: {
        'cardId': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
        'accountNo': PactMatchers.SomethingLike('0443482226'),
        'accountType': PactMatchers.SomethingLike('MAIN_ACCOUNT'),
        'customerId':
            PactMatchers.SomethingLike('ee6d61a7-7482-4c7a-a7db-29b774690375'),
        'cardType': PactMatchers.SomethingLike('DD1'),
        'cardPicture': PactMatchers.SomethingLike(
            'https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png'),
        'namePrinted': PactMatchers.SomethingLike(false),
        'dynamicCVV': PactMatchers.SomethingLike(false),
        'internationalBlockingFlag': PactMatchers.SomethingLike(false),
        'maskingCardNo': PactMatchers.SomethingLike('0435411490164376'),
        'expiration': PactMatchers.SomethingLike('20270902'),
        'cardStatus': PactMatchers.SomethingLike('LOCKED'),
        'orderingNumber': PactMatchers.SomethingLike(1),
        'key': PactMatchers.SomethingLike('TJD0ZsH'),
        'createdAt': null,
        'updatedAt': null,
        'embossName': PactMatchers.Term(CHARACTER_REGEX, 'Maria Santos'),
        'cardDelivery': null,
        'cardLimit': {
          'id': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
          'limitAmountCardTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveCardTransaction': PactMatchers.SomethingLike(true),
          'limitAmountAtmWithdrawal': PactMatchers.SomethingLike(10000.0),
          'isActiveAtmWithdrawal': PactMatchers.SomethingLike(true),
          'limitAmountOnlineTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveOnlineTransaction': PactMatchers.SomethingLike(true),
          'createdAt': null,
          'updatedAt': null
        },
      },
    );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.freezeCard(
      cardId: cardId,
      customerId: customerId,
    );

    expect(
      cards.cardStatus.status,
      equals('LOCKED'),
      reason: 'Pact did respond with expected body',
    );

    pact.writePactFile(directory: directory);
  });

  test('POST : /v2/cards/{cardId}/update-limit', () async {
    const cardId = '08d8a7b9-5259-430d-8374-076592bfb4f5';
    const customerId = 'ee6d61a7-7482-4c7a-a7db-29b774690375';

    const limitAmountCardTransaction = 10000.0;
    const limitAmountAtmWithdrawal = 10000.0;
    const limitAmountOnlineTransaction = 10000.0;
    const isActiveCardTransaction = true;
    const isActiveAtmWithdrawal = true;
    const isActiveOnlineTransaction = true;
    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.post,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('POST : /v2/cards/{cardId}/update-limit')
        .uponReceiving('A request to update card limit by card id')
        .withRequest('POST', '/v2/cards/$cardId/update-limit', body: {
      'limitAmountCardTransaction': limitAmountCardTransaction,
      'limitAmountAtmWithdrawal': limitAmountAtmWithdrawal,
      'limitAmountOnlineTransaction': limitAmountOnlineTransaction,
      'isActiveCardTransaction': isActiveCardTransaction,
      'isActiveAtmWithdrawal': isActiveAtmWithdrawal,
      'isActiveOnlineTransaction': isActiveOnlineTransaction,
    }, headers: {
      'safi-cuid': '$customerId',
    }).willRespondWith(
      200,
      body: {
        'cardId': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
        'accountNo': PactMatchers.SomethingLike('0443482226'),
        'accountType': PactMatchers.SomethingLike('MAIN_ACCOUNT'),
        'customerId':
            PactMatchers.SomethingLike('ee6d61a7-7482-4c7a-a7db-29b774690375'),
        'cardType': PactMatchers.SomethingLike('DD1'),
        'cardPicture': PactMatchers.SomethingLike(
            'https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png'),
        'namePrinted': PactMatchers.SomethingLike(false),
        'dynamicCVV': PactMatchers.SomethingLike(false),
        'internationalBlockingFlag': PactMatchers.SomethingLike(false),
        'maskingCardNo': PactMatchers.SomethingLike('0435411490164376'),
        'expiration': PactMatchers.SomethingLike('20270902'),
        'cardStatus': PactMatchers.SomethingLike('PRINTED'),
        'orderingNumber': PactMatchers.SomethingLike(1),
        'key': PactMatchers.SomethingLike('TJD0ZsH'),
        'createdAt': null,
        'updatedAt': null,
        'embossName': PactMatchers.Term(CHARACTER_REGEX, 'Maria Santos'),
        'cardDelivery': null,
        'cardLimit': {
          'id': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
          'limitAmountCardTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveCardTransaction': PactMatchers.SomethingLike(true),
          'limitAmountAtmWithdrawal': PactMatchers.SomethingLike(10000.0),
          'isActiveAtmWithdrawal': PactMatchers.SomethingLike(true),
          'limitAmountOnlineTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveOnlineTransaction': PactMatchers.SomethingLike(true),
          'createdAt': null,
          'updatedAt': null
        },
      },
    );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.updateCardLimit(
      customerId: customerId,
      cardLimit: MockCardModel.mockCardLimit,
    );

    expect(
      cards,
      cards,
      reason: 'Pact did respond with expected body',
    );

    pact.writePactFile(
      directory: directory,
    );
  });

  test('POST : /v2/cards/sign-url', () async {
    const url =
        'https://storage.googleapis.com/test-safi-dev/8f7f8a2c-14a3-422d-b1dd-6abc6325597d/8f7f8a2c-14a3-422d-b1dd-6abc6325597d_7935d71c-9912-47ce-ab54-5f9ac0635e8f.png';
    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.post,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('POST : /v2/cards/sign-url')
        .uponReceiving('A request to get card images')
        .withRequest('POST', '/v2/cards/sign-url', body: {
      'url': url,
    }).willRespondWith(
      200,
      body: {
        'url': PactMatchers.SomethingLike(
            'https://storage.googleapis.com/test-safi-dev/8f7f8a2c-14a3-422d-b1dd-6abc6325597d/8f7f8a2c-14a3-422d-b1dd-6abc6325597d_7935d71c-9912-47ce-ab54-5f9ac0635e8f.png?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=card-manager%40acquired-badge-348405.iam.gserviceaccount.com%2F20221023%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20221023T135804Z&X-Goog-Expires=600&X-Goog-SignedHeaders=host&X-Goog-Signature=74bd46a5dae8b8a41ab93b9a17ce3a32adfb547bf135d62f83af92068babc0ef61c88b0fb8ccccd5f96ce9fe3535c5d2a5ca0ae9ccc74c1ea652d6208b2ad409c83bfd3e7fbc91c0168ffe7f94c66ed259628e81deba08c53ada241acb1b0f1668008b77f7272345e4a856f543408ff861c88cd7320fc96738cc679e0512ed36c5671111e6ff2a0967026e0c4f9540d71c0a7f3bf2384aaa8da84d86dc1dbb472aadaedb65302f6cf325a0d8ec809fdd41dd697048a93d8171767946de0a1d36beefbfc1c9bb962b28b4be230d00cb18a30ba383c346db3034d4907ced033b0ff78e0296a6507e3ba172eb46b96fdd22f94f8f935f0d0d0eb41a4c1f57f0e336')
      },
    );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.getCardImage(url);

    expect(
      cards,
      equals(cards),
      reason: 'Pact did respond with expected body',
    );

    pact.writePactFile(directory: directory);
  });

  test('POST : /v2/cards/{customerId}/activate', () async {
    const lastCardNumber = '164376';
    const pin = '112233';
    const cardId = '08d8a7b9-5259-430d-8374-076592bfb4f5';
    const customerId = 'ee6d61a7-7482-4c7a-a7db-29b774690375';
    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.post,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('POST : /v2/cards/{customerId}/activate')
        .uponReceiving('A request to activated card by customer id')
        .withRequest('POST', '/v2/cards/$customerId/activate', body: {
      'cardId': cardId,
      'lastCardNumber': lastCardNumber,
      'pin': pin,
    }).willRespondWith(
      200,
      body: {
        'cardId': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
        'accountNo': PactMatchers.SomethingLike('0443482226'),
        'accountType': PactMatchers.SomethingLike('MAIN_ACCOUNT'),
        'customerId':
            PactMatchers.SomethingLike('ee6d61a7-7482-4c7a-a7db-29b774690375'),
        'cardType': PactMatchers.SomethingLike('DD1'),
        'cardPicture': PactMatchers.SomethingLike(
            'https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png'),
        'namePrinted': PactMatchers.SomethingLike(false),
        'dynamicCVV': PactMatchers.SomethingLike(false),
        'internationalBlockingFlag': PactMatchers.SomethingLike(false),
        'maskingCardNo': PactMatchers.SomethingLike('0435411490164376'),
        'expiration': PactMatchers.SomethingLike('20270902'),
        'cardStatus': PactMatchers.SomethingLike('ACTIVE'),
        'orderingNumber': PactMatchers.SomethingLike(1),
        'key': PactMatchers.SomethingLike('TJD0ZsH'),
        'createdAt': null,
        'updatedAt': null,
        'embossName': PactMatchers.Term(CHARACTER_REGEX, 'Maria Santos'),
        'cardDelivery': null,
        'cardLimit': null,
      },
    );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.activatedCard(
      cardId: cardId,
      customerId: customerId,
      lastCardNumber: lastCardNumber,
      pin: pin,
    );

    expect(
      cards.cardId,
      cardId,
      reason: 'Pact did respond with expected body',
    );

    pact.writePactFile(directory: directory);
  });

  test('POST - UnfreezeCard : /v2/cards/{cardId}/card-lock', () async {
    const cardId = '08d8a7b9-5259-430d-8374-076592bfb4f5';
    const customerId = 'ee6d61a7-7482-4c7a-a7db-29b774690375';
    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.post,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('POST - UnfreezeCard : /v2/cards/{cardId}/card-lock')
        .uponReceiving('A request to unfreeze cards by card id')
        .withRequest('POST', '/v2/cards/$cardId/card-lock', body: {
      'function': 'UNFREEZE',
    }, headers: {
      'safi-cuid': '$customerId',
    }).willRespondWith(
      200,
      body: {
        'cardId': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
        'accountNo': PactMatchers.SomethingLike('0443482226'),
        'accountType': PactMatchers.SomethingLike('MAIN_ACCOUNT'),
        'customerId':
            PactMatchers.SomethingLike('ee6d61a7-7482-4c7a-a7db-29b774690375'),
        'cardType': PactMatchers.SomethingLike('DD1'),
        'cardPicture': PactMatchers.SomethingLike(
            'https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png'),
        'namePrinted': PactMatchers.SomethingLike(false),
        'dynamicCVV': PactMatchers.SomethingLike(false),
        'internationalBlockingFlag': PactMatchers.SomethingLike(false),
        'maskingCardNo': PactMatchers.SomethingLike('0435411490164376'),
        'expiration': PactMatchers.SomethingLike('20270902'),
        'cardStatus': PactMatchers.SomethingLike('ACTIVE'),
        'orderingNumber': PactMatchers.SomethingLike(1),
        'key': PactMatchers.SomethingLike('TJD0ZsH'),
        'createdAt': null,
        'updatedAt': null,
        'embossName': PactMatchers.Term(CHARACTER_REGEX, 'Maria Santos'),
        'cardDelivery': null,
        'cardLimit': {
          'id': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
          'limitAmountCardTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveCardTransaction': PactMatchers.SomethingLike(true),
          'limitAmountAtmWithdrawal': PactMatchers.SomethingLike(10000.0),
          'isActiveAtmWithdrawal': PactMatchers.SomethingLike(true),
          'limitAmountOnlineTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveOnlineTransaction': PactMatchers.SomethingLike(true),
          'createdAt': null,
          'updatedAt': null
        },
      },
    );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.unfreezeCard(
      cardId: cardId,
      customerId: customerId,
    );

    expect(
      cards.cardStatus.status,
      equals('ACTIVE'),
      reason: 'Pact did respond with expected body',
    );

    pact.writePactFile(directory: directory);
  });

  test('GET : /v2/cards/{cardId}/details', () async {
    const cardId = '08d8a7b9-5259-430d-8374-076592bfb4f5';
    const customerId = 'ee6d61a7-7482-4c7a-a7db-29b774690375';
    const isSyncTracking = false;
    const isIncludeDelivery = false;

    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.get,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('GET : /v2/cards/{cardId}/details')
        .uponReceiving('A request to get card details by card id')
        .withRequest('GET', '/v2/cards/$cardId/details', headers: {
      'safi-cuid': '$customerId',
    }, query: {
      'isSyncTracking': isSyncTracking.toString(),
      'isIncludeDelivery': isIncludeDelivery.toString(),
    }).willRespondWith(
      200,
      body: {
        'cardId': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
        'accountNo': PactMatchers.SomethingLike('0443482226'),
        'accountType': PactMatchers.SomethingLike('MAIN_ACCOUNT'),
        'customerId':
            PactMatchers.SomethingLike('ee6d61a7-7482-4c7a-a7db-29b774690375'),
        'cardType': PactMatchers.SomethingLike('DD1'),
        'cardPicture': PactMatchers.SomethingLike(
            'https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png'),
        'namePrinted': PactMatchers.SomethingLike(false),
        'dynamicCVV': PactMatchers.SomethingLike(false),
        'internationalBlockingFlag': PactMatchers.SomethingLike(false),
        'maskingCardNo': PactMatchers.SomethingLike('0435411490164376'),
        'expiration': PactMatchers.SomethingLike('20270902'),
        'cardStatus': PactMatchers.SomethingLike('ACTIVE'),
        'orderingNumber': PactMatchers.SomethingLike(1),
        'key': PactMatchers.SomethingLike('TJD0ZsH'),
        'createdAt': null,
        'updatedAt': null,
        'embossName': PactMatchers.Term(CHARACTER_REGEX, 'Maria Santos'),
        'cardDelivery': null,
        'cardLimit': {
          'id': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
          'limitAmountCardTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveCardTransaction': PactMatchers.SomethingLike(true),
          'limitAmountAtmWithdrawal': PactMatchers.SomethingLike(10000.0),
          'isActiveAtmWithdrawal': PactMatchers.SomethingLike(true),
          'limitAmountOnlineTransaction': PactMatchers.SomethingLike(10000.0),
          'isActiveOnlineTransaction': PactMatchers.SomethingLike(true),
          'createdAt': null,
          'updatedAt': null
        },
      },
    );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.getCardById(
      cardId,
      isSyncTracking,
      isIncludeDelivery,
    );

    expect(
      cards.cardId,
      cardId,
      reason: 'Pact did respond with expected body',
    );

    pact.writePactFile(directory: directory);
  });

  test('POST : /v2/cards/change-pin', () async {
    const cardId = '08d8a7b9-5259-430d-8374-076592bfb4f5';
    const currentPin = '123123';
    const newPin = '112233';
    const customerId = 'ee6d61a7-7482-4c7a-a7db-29b774690375';
    when(() => requestSigner.createHeaders(
          body: any(named: 'body'),
          signingLevel: any(named: 'signingLevel'),
          userPresenceCheckArgs: any(named: 'userPresenceCheckArgs'),
          path: any(named: 'path'),
          method: HttpMethodType.post,
        )).thenAnswer((_) async => {
          'safi-cuid': mockSafiCuid,
        });

    pact
        .newInteraction()
        .given('POST : /v2/cards/change-pin')
        .uponReceiving('A request to change pin card')
        .withRequest('POST', '/v2/cards/change-pin', body: {
      'cardId': cardId,
      'newPin': newPin,
      'oldPin': currentPin,
    }, headers: {
      'safi-cuid': '$customerId',
    }).willRespondWith(
      200,
      body: {
        'cardId': PactMatchers.uuid('08d8a7b9-5259-430d-8374-076592bfb4f5'),
        'accountNo': PactMatchers.SomethingLike('0443482226'),
        'accountType': PactMatchers.SomethingLike('MAIN_ACCOUNT'),
        'customerId':
            PactMatchers.SomethingLike('ee6d61a7-7482-4c7a-a7db-29b774690375'),
        'cardType': PactMatchers.SomethingLike('DD1'),
        'cardPicture': PactMatchers.SomethingLike(
            'https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png'),
        'namePrinted': PactMatchers.SomethingLike(false),
        'dynamicCVV': PactMatchers.SomethingLike(false),
        'internationalBlockingFlag': PactMatchers.SomethingLike(false),
        'maskingCardNo': PactMatchers.SomethingLike('0435411490164376'),
        'expiration': PactMatchers.SomethingLike('20270902'),
        'cardStatus': PactMatchers.SomethingLike('ACTIVE'),
        'orderingNumber': PactMatchers.SomethingLike(1),
        'key': PactMatchers.SomethingLike('TJD0ZsH'),
        'createdAt': null,
        'updatedAt': null,
        'embossName': PactMatchers.Term(CHARACTER_REGEX, 'Maria Santos'),
        'cardDelivery': null,
        'cardLimit': null,
      },
    );

    pact.run(secure: false);

    final cards = await cardRemoteDatasource.changePin(
      customerId: customerId,
      cardId: cardId,
      newPin: newPin,
      currentPin: currentPin,
    );

    expect(
      cards,
      cards,
      reason: 'Pact did respond with expected body',
    );

    pact.writePactFile(directory: directory);
  });
}

class MockRequestSigner extends Mock implements RequestSigner {}
Pact JSON Contract : safi-mobile-app-card-manager.json
{
  "consumer": {
    "name": "safi-mobile-app"
  },
  "interactions": [
    {
      "description": "A request to get card details by card id",
      "providerStates": [
        {
          "name": "GET : /v2/cards/{cardId}/details"
        }
      ],
      "request": {
        "headers": {
          "safi-cuid": "ee6d61a7-7482-4c7a-a7db-29b774690375"
        },
        "method": "GET",
        "path": "/v2/cards/08d8a7b9-5259-430d-8374-076592bfb4f5/details",
        "query": {
          "isIncludeDelivery": [
            "false"
          ],
          "isSyncTracking": [
            "false"
          ]
        }
      },
      "response": {
        "body": {
          "accountNo": "0443482226",
          "accountType": "MAIN_ACCOUNT",
          "cardDelivery": null,
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "cardLimit": {
            "createdAt": null,
            "id": "08d8a7b9-5259-430d-8374-076592bfb4f5",
            "isActiveAtmWithdrawal": true,
            "isActiveCardTransaction": true,
            "isActiveOnlineTransaction": true,
            "limitAmountAtmWithdrawal": 10000.0,
            "limitAmountCardTransaction": 10000.0,
            "limitAmountOnlineTransaction": 10000.0,
            "updatedAt": null
          },
          "cardPicture": "https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png",
          "cardStatus": "ACTIVE",
          "cardType": "DD1",
          "createdAt": null,
          "customerId": "ee6d61a7-7482-4c7a-a7db-29b774690375",
          "dynamicCVV": false,
          "embossName": "Maria Santos",
          "expiration": "20270902",
          "internationalBlockingFlag": false,
          "key": "TJD0ZsH",
          "maskingCardNo": "0435411490164376",
          "namePrinted": false,
          "orderingNumber": 1,
          "updatedAt": null
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$.accountNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.accountType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.id": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.isActiveAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardPicture": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardStatus": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.customerId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.dynamicCVV": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.embossName": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[a-zA-Z ]*$"
                }
              ]
            },
            "$.expiration": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.internationalBlockingFlag": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.key": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.maskingCardNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.namePrinted": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.orderingNumber": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "A request to get cards by card id",
      "providerStates": [
        {
          "name": "GET : /v2/cards/{customerId}"
        }
      ],
      "request": {
        "method": "GET",
        "path": "/v2/cards/ee6d61a7-7482-4c7a-a7db-29b774690375"
      },
      "response": {
        "body": [
          {
            "accountNo": "0443482226",
            "accountType": "MAIN_ACCOUNT",
            "cardDelivery": null,
            "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
            "cardLimit": null,
            "cardPicture": "https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png",
            "cardStatus": "ACTIVE",
            "cardType": "DD1",
            "createdAt": null,
            "customerId": "ee6d61a7-7482-4c7a-a7db-29b774690375",
            "dynamicCVV": false,
            "embossName": "Maria Santos",
            "expiration": "20270902",
            "internationalBlockingFlag": false,
            "key": "TJD0ZsH",
            "maskingCardNo": "0435411490164376",
            "namePrinted": false,
            "orderingNumber": 1,
            "updatedAt": null
          }
        ],
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type",
                  "min": 1
                }
              ]
            },
            "$[*].accountNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].accountType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].cardId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$[*].cardPicture": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].cardStatus": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].cardType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].customerId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].dynamicCVV": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].embossName": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[a-zA-Z ]*$"
                }
              ]
            },
            "$[*].expiration": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].internationalBlockingFlag": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].key": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].maskingCardNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].namePrinted": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$[*].orderingNumber": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "integer"
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "A request to block card by card id",
      "providerStates": [
        {
          "name": "POST - BlockCard : /v2/cards/{cardId}/card-lock"
        }
      ],
      "request": {
        "body": {
          "function": "BLOCK"
        },
        "headers": {
          "Content-Type": "application/json",
          "safi-cuid": "ee6d61a7-7482-4c7a-a7db-29b774690375"
        },
        "method": "POST",
        "path": "/v2/cards/08d8a7b9-5259-430d-8374-076592bfb4f5/card-lock"
      },
      "response": {
        "body": {
          "accountNo": "0443482226",
          "accountType": "MAIN_ACCOUNT",
          "cardDelivery": null,
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "cardLimit": {
            "createdAt": null,
            "id": "08d8a7b9-5259-430d-8374-076592bfb4f5",
            "isActiveAtmWithdrawal": true,
            "isActiveCardTransaction": true,
            "isActiveOnlineTransaction": true,
            "limitAmountAtmWithdrawal": 10000.0,
            "limitAmountCardTransaction": 10000.0,
            "limitAmountOnlineTransaction": 10000.0,
            "updatedAt": null
          },
          "cardPicture": "https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png",
          "cardStatus": "PERMANENT_BLOCKED",
          "cardType": "DD1",
          "createdAt": null,
          "customerId": "ee6d61a7-7482-4c7a-a7db-29b774690375",
          "dynamicCVV": false,
          "embossName": "Maria Santos",
          "expiration": "20270902",
          "internationalBlockingFlag": false,
          "key": "TJD0ZsH",
          "maskingCardNo": "0435411490164376",
          "namePrinted": false,
          "orderingNumber": 1,
          "updatedAt": null
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$.accountNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.accountType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.id": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.isActiveAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardPicture": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardStatus": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.customerId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.dynamicCVV": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.embossName": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[a-zA-Z ]*$"
                }
              ]
            },
            "$.expiration": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.internationalBlockingFlag": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.key": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.maskingCardNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.namePrinted": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.orderingNumber": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "A request to freeze card by card id",
      "providerStates": [
        {
          "name": "POST - FreezeCard : /v2/cards/{cardId}/card-lock"
        }
      ],
      "request": {
        "body": {
          "function": "FREEZE"
        },
        "headers": {
          "Content-Type": "application/json",
          "safi-cuid": "ee6d61a7-7482-4c7a-a7db-29b774690375"
        },
        "method": "POST",
        "path": "/v2/cards/08d8a7b9-5259-430d-8374-076592bfb4f5/card-lock"
      },
      "response": {
        "body": {
          "accountNo": "0443482226",
          "accountType": "MAIN_ACCOUNT",
          "cardDelivery": null,
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "cardLimit": {
            "createdAt": null,
            "id": "08d8a7b9-5259-430d-8374-076592bfb4f5",
            "isActiveAtmWithdrawal": true,
            "isActiveCardTransaction": true,
            "isActiveOnlineTransaction": true,
            "limitAmountAtmWithdrawal": 10000.0,
            "limitAmountCardTransaction": 10000.0,
            "limitAmountOnlineTransaction": 10000.0,
            "updatedAt": null
          },
          "cardPicture": "https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png",
          "cardStatus": "LOCKED",
          "cardType": "DD1",
          "createdAt": null,
          "customerId": "ee6d61a7-7482-4c7a-a7db-29b774690375",
          "dynamicCVV": false,
          "embossName": "Maria Santos",
          "expiration": "20270902",
          "internationalBlockingFlag": false,
          "key": "TJD0ZsH",
          "maskingCardNo": "0435411490164376",
          "namePrinted": false,
          "orderingNumber": 1,
          "updatedAt": null
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$.accountNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.accountType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.id": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.isActiveAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardPicture": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardStatus": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.customerId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.dynamicCVV": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.embossName": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[a-zA-Z ]*$"
                }
              ]
            },
            "$.expiration": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.internationalBlockingFlag": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.key": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.maskingCardNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.namePrinted": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.orderingNumber": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "A request to unfreeze cards by card id",
      "providerStates": [
        {
          "name": "POST - UnfreezeCard : /v2/cards/{cardId}/card-lock"
        }
      ],
      "request": {
        "body": {
          "function": "UNFREEZE"
        },
        "headers": {
          "Content-Type": "application/json",
          "safi-cuid": "ee6d61a7-7482-4c7a-a7db-29b774690375"
        },
        "method": "POST",
        "path": "/v2/cards/08d8a7b9-5259-430d-8374-076592bfb4f5/card-lock"
      },
      "response": {
        "body": {
          "accountNo": "0443482226",
          "accountType": "MAIN_ACCOUNT",
          "cardDelivery": null,
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "cardLimit": {
            "createdAt": null,
            "id": "08d8a7b9-5259-430d-8374-076592bfb4f5",
            "isActiveAtmWithdrawal": true,
            "isActiveCardTransaction": true,
            "isActiveOnlineTransaction": true,
            "limitAmountAtmWithdrawal": 10000.0,
            "limitAmountCardTransaction": 10000.0,
            "limitAmountOnlineTransaction": 10000.0,
            "updatedAt": null
          },
          "cardPicture": "https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png",
          "cardStatus": "ACTIVE",
          "cardType": "DD1",
          "createdAt": null,
          "customerId": "ee6d61a7-7482-4c7a-a7db-29b774690375",
          "dynamicCVV": false,
          "embossName": "Maria Santos",
          "expiration": "20270902",
          "internationalBlockingFlag": false,
          "key": "TJD0ZsH",
          "maskingCardNo": "0435411490164376",
          "namePrinted": false,
          "orderingNumber": 1,
          "updatedAt": null
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$.accountNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.accountType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.id": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.isActiveAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardPicture": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardStatus": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.customerId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.dynamicCVV": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.embossName": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[a-zA-Z ]*$"
                }
              ]
            },
            "$.expiration": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.internationalBlockingFlag": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.key": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.maskingCardNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.namePrinted": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.orderingNumber": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "A request to change pin card",
      "providerStates": [
        {
          "name": "POST : /v2/cards/change-pin"
        }
      ],
      "request": {
        "body": {
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "newPin": "112233",
          "oldPin": "123123"
        },
        "headers": {
          "Content-Type": "application/json",
          "safi-cuid": "ee6d61a7-7482-4c7a-a7db-29b774690375"
        },
        "method": "POST",
        "path": "/v2/cards/change-pin"
      },
      "response": {
        "body": {
          "accountNo": "0443482226",
          "accountType": "MAIN_ACCOUNT",
          "cardDelivery": null,
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "cardLimit": null,
          "cardPicture": "https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png",
          "cardStatus": "ACTIVE",
          "cardType": "DD1",
          "createdAt": null,
          "customerId": "ee6d61a7-7482-4c7a-a7db-29b774690375",
          "dynamicCVV": false,
          "embossName": "Maria Santos",
          "expiration": "20270902",
          "internationalBlockingFlag": false,
          "key": "TJD0ZsH",
          "maskingCardNo": "0435411490164376",
          "namePrinted": false,
          "orderingNumber": 1,
          "updatedAt": null
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$.accountNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.accountType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardPicture": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardStatus": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.customerId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.dynamicCVV": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.embossName": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[a-zA-Z ]*$"
                }
              ]
            },
            "$.expiration": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.internationalBlockingFlag": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.key": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.maskingCardNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.namePrinted": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.orderingNumber": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "A request to get card images",
      "providerStates": [
        {
          "name": "POST : /v2/cards/sign-url"
        }
      ],
      "request": {
        "body": {
          "url": "https://storage.googleapis.com/test-safi-dev/8f7f8a2c-14a3-422d-b1dd-6abc6325597d/8f7f8a2c-14a3-422d-b1dd-6abc6325597d_7935d71c-9912-47ce-ab54-5f9ac0635e8f.png"
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "method": "POST",
        "path": "/v2/cards/sign-url"
      },
      "response": {
        "body": {
          "url": "https://storage.googleapis.com/test-safi-dev/8f7f8a2c-14a3-422d-b1dd-6abc6325597d/8f7f8a2c-14a3-422d-b1dd-6abc6325597d_7935d71c-9912-47ce-ab54-5f9ac0635e8f.png?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=card-manager%40acquired-badge-348405.iam.gserviceaccount.com%2F20221023%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20221023T135804Z&X-Goog-Expires=600&X-Goog-SignedHeaders=host&X-Goog-Signature=74bd46a5dae8b8a41ab93b9a17ce3a32adfb547bf135d62f83af92068babc0ef61c88b0fb8ccccd5f96ce9fe3535c5d2a5ca0ae9ccc74c1ea652d6208b2ad409c83bfd3e7fbc91c0168ffe7f94c66ed259628e81deba08c53ada241acb1b0f1668008b77f7272345e4a856f543408ff861c88cd7320fc96738cc679e0512ed36c5671111e6ff2a0967026e0c4f9540d71c0a7f3bf2384aaa8da84d86dc1dbb472aadaedb65302f6cf325a0d8ec809fdd41dd697048a93d8171767946de0a1d36beefbfc1c9bb962b28b4be230d00cb18a30ba383c346db3034d4907ced033b0ff78e0296a6507e3ba172eb46b96fdd22f94f8f935f0d0d0eb41a4c1f57f0e336"
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$.url": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "A request to change card name",
      "providerStates": [
        {
          "name": "POST : /v2/cards/{cardId}/change-name"
        }
      ],
      "request": {
        "body": {
          "newCardName": "Maria Santos"
        },
        "headers": {
          "Content-Type": "application/json",
          "safi-cuid": "ee6d61a7-7482-4c7a-a7db-29b774690375"
        },
        "method": "POST",
        "path": "/v2/cards/08d8a7b9-5259-430d-8374-076592bfb4f5/change-name"
      },
      "response": {
        "body": {
          "accountNo": "0443482226",
          "accountType": "MAIN_ACCOUNT",
          "cardDelivery": null,
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "cardLimit": null,
          "cardPicture": "https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png",
          "cardStatus": "ACTIVE",
          "cardType": "DD1",
          "createdAt": null,
          "customerId": "ee6d61a7-7482-4c7a-a7db-29b774690375",
          "dynamicCVV": false,
          "embossName": "Maria Santos",
          "expiration": "20270902",
          "internationalBlockingFlag": false,
          "key": "TJD0ZsH",
          "maskingCardNo": "0435411490164376",
          "namePrinted": false,
          "orderingNumber": 1,
          "updatedAt": null
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$.accountNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.accountType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardPicture": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardStatus": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.customerId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.dynamicCVV": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.embossName": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[a-zA-Z ]*$"
                }
              ]
            },
            "$.expiration": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.internationalBlockingFlag": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.key": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.maskingCardNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.namePrinted": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.orderingNumber": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "A request to update card limit by card id",
      "providerStates": [
        {
          "name": "POST : /v2/cards/{cardId}/update-limit"
        }
      ],
      "request": {
        "body": {
          "isActiveAtmWithdrawal": true,
          "isActiveCardTransaction": true,
          "isActiveOnlineTransaction": true,
          "limitAmountAtmWithdrawal": 10000.0,
          "limitAmountCardTransaction": 10000.0,
          "limitAmountOnlineTransaction": 10000.0
        },
        "headers": {
          "Content-Type": "application/json",
          "safi-cuid": "ee6d61a7-7482-4c7a-a7db-29b774690375"
        },
        "method": "POST",
        "path": "/v2/cards/08d8a7b9-5259-430d-8374-076592bfb4f5/update-limit"
      },
      "response": {
        "body": {
          "accountNo": "0443482226",
          "accountType": "MAIN_ACCOUNT",
          "cardDelivery": null,
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "cardLimit": {
            "createdAt": null,
            "id": "08d8a7b9-5259-430d-8374-076592bfb4f5",
            "isActiveAtmWithdrawal": true,
            "isActiveCardTransaction": true,
            "isActiveOnlineTransaction": true,
            "limitAmountAtmWithdrawal": 10000.0,
            "limitAmountCardTransaction": 10000.0,
            "limitAmountOnlineTransaction": 10000.0,
            "updatedAt": null
          },
          "cardPicture": "https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png",
          "cardStatus": "PRINTED",
          "cardType": "DD1",
          "createdAt": null,
          "customerId": "ee6d61a7-7482-4c7a-a7db-29b774690375",
          "dynamicCVV": false,
          "embossName": "Maria Santos",
          "expiration": "20270902",
          "internationalBlockingFlag": false,
          "key": "TJD0ZsH",
          "maskingCardNo": "0435411490164376",
          "namePrinted": false,
          "orderingNumber": 1,
          "updatedAt": null
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$.accountNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.accountType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.id": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardLimit.isActiveAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.isActiveOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountAtmWithdrawal": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountCardTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardLimit.limitAmountOnlineTransaction": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardPicture": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardStatus": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.customerId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.dynamicCVV": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.embossName": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[a-zA-Z ]*$"
                }
              ]
            },
            "$.expiration": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.internationalBlockingFlag": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.key": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.maskingCardNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.namePrinted": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.orderingNumber": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    },
    {
      "description": "A request to activated card by customer id",
      "providerStates": [
        {
          "name": "POST : /v2/cards/{customerId}/activate"
        }
      ],
      "request": {
        "body": {
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "lastCardNumber": "164376",
          "pin": "112233"
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "method": "POST",
        "path": "/v2/cards/ee6d61a7-7482-4c7a-a7db-29b774690375/activate"
      },
      "response": {
        "body": {
          "accountNo": "0443482226",
          "accountType": "MAIN_ACCOUNT",
          "cardDelivery": null,
          "cardId": "08d8a7b9-5259-430d-8374-076592bfb4f5",
          "cardLimit": null,
          "cardPicture": "https://storage.googleapis.com/test-safi-dev/ee6d61a7-7482-4c7a-a7db-29b774690375/ee6d61a7-7482-4c7a-a7db-29b774690375_08d8a7b9-5259-430d-8374-076592bfb4f5.png",
          "cardStatus": "ACTIVE",
          "cardType": "DD1",
          "createdAt": null,
          "customerId": "ee6d61a7-7482-4c7a-a7db-29b774690375",
          "dynamicCVV": false,
          "embossName": "Maria Santos",
          "expiration": "20270902",
          "internationalBlockingFlag": false,
          "key": "TJD0ZsH",
          "maskingCardNo": "0435411490164376",
          "namePrinted": false,
          "orderingNumber": 1,
          "updatedAt": null
        },
        "headers": {
          "Content-Type": "application/json"
        },
        "matchingRules": {
          "body": {
            "$.accountNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.accountType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}$"
                }
              ]
            },
            "$.cardPicture": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardStatus": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.cardType": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.customerId": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.dynamicCVV": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.embossName": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "regex",
                  "regex": "^[a-zA-Z ]*$"
                }
              ]
            },
            "$.expiration": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.internationalBlockingFlag": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.key": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.maskingCardNo": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.namePrinted": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            },
            "$.orderingNumber": {
              "combine": "AND",
              "matchers": [
                {
                  "match": "type"
                }
              ]
            }
          }
        },
        "status": 200
      }
    }
  ],
  "metadata": {
    "pactRust": {
      "version": "0.1.2"
    },
    "pactSpecification": {
      "version": "3.0.0"
    }
  },
  "provider": {
    "name": "card-manager"
  }
}

Provider Pact Test

In provider verification, each request is sent to the provider, and the actual response it generates is compared with the minimal expected response described in the consumer test.

Provider verification passes if each request generates a response that contains at least the data described in the minimal expected response.
To simulate the provider behaviour we have created a provider service using Micronaut.

Steps :

  1. We need to set it up first, for example like this picture in the build.gradle.kts

  2. And then create a provider service file

  3. Annotated with @Tag("contract-verification")

  4. Provider states allow you to set up data on the provider before the interaction is run, so that it can make a response that matches what the consumer expects.

contract_test/provider/CardContractVerificationTest.kt
package ph.safibank.cardmanager.contract_test.provider

import au.com.dius.pact.provider.junit.Provider
import au.com.dius.pact.provider.junit.State
import au.com.dius.pact.provider.junit.StateChangeAction
import au.com.dius.pact.provider.junit.loader.PactBroker
import au.com.dius.pact.provider.junit.loader.PactBrokerAuth
import au.com.dius.pact.provider.junit5.HttpTestTarget
import au.com.dius.pact.provider.junit5.PactVerificationContext
import au.com.dius.pact.provider.junit5.PactVerificationInvocationContextProvider
import io.micronaut.test.annotation.MockBean
import io.micronaut.test.extensions.junit5.annotation.MicronautTest
import io.mockk.clearAllMocks
import io.mockk.every
import io.mockk.mockk
import jakarta.inject.Inject
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.TestTemplate
import org.junit.jupiter.api.extension.ExtendWith
import ph.safibank.cardmanager.common.ParentLocalTest
import ph.safibank.cardmanager.mock.CardMock
import ph.safibank.cardmanager.service.BucketService
import ph.safibank.cardmanager.service.CardService
import ph.safibank.cardmanager.service.impl.BucketServiceImpl
import ph.safibank.cardmanager.service.impl.CardServiceImpl
import java.net.URL

@Tag("contract-verification")
@MicronautTest
@Provider("card-manager")
@PactBroker(authentication = PactBrokerAuth(token = "\${pactbroker.auth.token}"))

class CardContractVerificationTest {
    @Inject
    lateinit var cardService: CardService

    @Inject
    lateinit var bucketService: BucketService

    @MockBean(CardServiceImpl::class)
    fun cardService(): CardService = mockk<CardService>()

    @MockBean(BucketServiceImpl::class)
    fun bucketService(): BucketService = mockk<BucketService>()

    private val cardMock: CardMock = CardMock()

    @TestTemplate
    @ExtendWith(PactVerificationInvocationContextProvider::class)
    fun pactVerificationTestTemplate(context: PactVerificationContext) {
        context.verifyInteraction()
        context.target = HttpTestTarget()
    }

    @BeforeEach
    fun setup() {
        clearAllMocks()
        every {
            bucketService.getImageUrl(any())
        } returns URL("https://storage.googleapis.com/test-safi-dev/8f7f8a2c-14a3-422d-b1dd-6abc6325597d/8f7f8a2c-14a3-422d-b1dd-6abc6325597d_7935d71c-9912-47ce-ab54-5f9ac0635e8f.png?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=card-manager%40acquired-badge-348405.iam.gserviceaccount.com%2F20221023%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20221023T135804Z&X-Goog-Expires=600&X-Goog-SignedHeaders=host&X-Goog-Signature=74bd46a5dae8b8a41ab93b9a17ce3a32adfb547bf135d62f83af92068babc0ef61c88b0fb8ccccd5f96ce9fe3535c5d2a5ca0ae9ccc74c1ea652d6208b2ad409c83bfd3e7fbc91c0168ffe7f94c66ed259628e81deba08c53ada241acb1b0f1668008b77f7272345e4a856f543408ff861c88cd7320fc96738cc679e0512ed36c5671111e6ff2a0967026e0c4f9540d71c0a7f3bf2384aaa8da84d86dc1dbb472aadaedb65302f6cf325a0d8ec809fdd41dd697048a93d8171767946de0a1d36beefbfc1c9bb962b28b4be230d00cb18a30ba383c346db3034d4907ced033b0ff78e0296a6507e3ba172eb46b96fdd22f94f8f935f0d0d0eb41a4c1f57f0e336")
        every {
            bucketService.getObjectsByDirectory(any())
        } returns listOf()
    }

    @State(value = ["GET : /v2/cards/{customerId}"])
    fun getCards() {
        every {
            cardService.getAllByCustomerId(any())
        } returns listOf(cardMock.getCardEntity().toCard())
    }

    @State(value = ["POST : /v2/cards/{cardId}/change-name"])
    fun changeCardName() {
        every {
            cardService.changeName(any())
        } returns cardMock.getChangeNameResponse().toCard()
    }

    @State(value = ["POST - BlockCard : /v2/cards/{cardId}/card-lock"])
    fun blockCard() {
        every {
            cardService.blockCard(any(), any(), any(), any())
        } returns cardMock.getCardEntity().toCard()
    }

    @State(value = ["POST - FreezeCard : /v2/cards/{cardId}/card-lock"])
    fun freezeCard() {
        every {
            cardService.freezeCard(any(), any(), any(), any())
        } returns cardMock.getCardEntity().toCard()
    }

    @State(value = ["POST - UnfreezeCard : /v2/cards/{cardId}/card-lock"])
    fun unFreezeCard() {
        every {
            cardService.unfreezeCard(any(), any(), any())
        } returns cardMock.getCardEntity().toCard()
    }

    @State(value = ["POST : /v2/cards/{cardId}/update-limit"])
    fun updateCardLimit() {
        every {
            cardService.updateCardLimit(any(), any(), any())
        } returns cardMock.getCardEntity().toCard()
    }

    @State(value = ["POST : /v2/cards/sign-url"])
    fun getCardImage() {
        every {
            bucketService.getImageUrl(any())
        } returns URL("https://storage.googleapis.com/test-safi-dev/8f7f8a2c-14a3-422d-b1dd-6abc6325597d/8f7f8a2c-14a3-422d-b1dd-6abc6325597d_7935d71c-9912-47ce-ab54-5f9ac0635e8f.png?X-Goog-Algorithm=GOOG4-RSA-SHA256&X-Goog-Credential=card-manager%40acquired-badge-348405.iam.gserviceaccount.com%2F20221023%2Fauto%2Fstorage%2Fgoog4_request&X-Goog-Date=20221023T135804Z&X-Goog-Expires=600&X-Goog-SignedHeaders=host&X-Goog-Signature=74bd46a5dae8b8a41ab93b9a17ce3a32adfb547bf135d62f83af92068babc0ef61c88b0fb8ccccd5f96ce9fe3535c5d2a5ca0ae9ccc74c1ea652d6208b2ad409c83bfd3e7fbc91c0168ffe7f94c66ed259628e81deba08c53ada241acb1b0f1668008b77f7272345e4a856f543408ff861c88cd7320fc96738cc679e0512ed36c5671111e6ff2a0967026e0c4f9540d71c0a7f3bf2384aaa8da84d86dc1dbb472aadaedb65302f6cf325a0d8ec809fdd41dd697048a93d8171767946de0a1d36beefbfc1c9bb962b28b4be230d00cb18a30ba383c346db3034d4907ced033b0ff78e0296a6507e3ba172eb46b96fdd22f94f8f935f0d0d0eb41a4c1f57f0e336")
    }

    @State(value = ["POST : /v2/cards/{customerId}/activate"])
    fun activatedCard() {
        every {
            cardService.activateCard(any())
        } returns cardMock.getCardEntity().toCard()
    }

    @State(value = ["POST : /v2/cards/change-pin"])
    fun changePin() {
        every {
            cardService.changePin(any())
        } returns cardMock.getCardEntity().toCard()
    }

    @State(value = ["GET : /v2/cards/{cardId}/details"])
    fun getCardById() {
        every {
            cardService.getCardByCardId(any(), any())
        } returns cardMock.getCardEntity().toCard()
    }
}

3. After that, we run this test, and if everything passes, the ouput should look like this:

That’s all from the provider side, and now we can see both sides are done, and contract tests are completed.

Can I Deploy

Before you deploy a new version of a service to a higher environment, you need to know whether or not the version you're about to deploy is compatible with the versions of the other services/apps that already exist in that environment.

Pactflow provides this feature to helps us deploy safely in practice.

This is an example of when we could deploy :

This is an example of when we couldn’t deploy :

We could trace the error with the click URL in the verification results.

Publish Contract Test to Pactflow With Github Actions

Now that we have created and verified our provider contract, we need to share the contract to our consumers. This is where Pactflow comes in to the picture. This step is referred to as "publishing" the provider contract.

We will publish our contracts to Pactflow or we call them pact brokers using github actions. Following are some of the steps to publish a contract.

Steps :

  1. First we should publish the consumer → pact-publish-consumer

  2. Second, publish the provider → pact-publish-provider

  3. Then, check if we can deploy the provider → pact-can-i-deploy-provider

  4. After that, it will record the deployment provider → pact-record-deployment-provider

  5. And then, it will create a tag for provider → pact-create-tag-provider

  6. After the provider is done, then it will check if we can deploy the consumer → pact-can-i-deploy-consumer

  7. Then, it will record the deployment consumer → pact-record-deployment-consumer

  8. And then, it will create a tag for consumer → pact-create-tag-consumer

safi-mobile-app-publish-contract-ci.yml
name: Publish Contract Test

on:
  workflow_call:
    inputs:
      environment-variable:
        required: true
        type: string
      microservice-name:
        required: true
        type: string
      consumer-name:
        required: true
        type: string
      provider-name:
        required: true
        type: string

env:
  VERSION: ${{ github.sha }}
  PACT_BROKER_BASE_URL: https://safi.pactflow.io
  PACT_BROKER_TOKEN: "s_nEbsQhRZseQHqFzhDfaQ"

jobs:
  pact-publish-consumer:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: pactflow/actions/publish-pact-files@v1.0.0
        env:
          version: ${{ env.VERSION }}
          pactfiles: ${{ github.workspace }}/app/app_safi/contract_test
          application_name: ${{ inputs.consumer-name }}

  pact-can-i-deploy-consumer:
    needs: pact-record-deployment-provider
    runs-on: ubuntu-latest
    steps:
      - uses: pactflow/actions/can-i-deploy@v0.0.4
        env:
          version: ${{ env.VERSION }}
          to_environment: ${{ inputs.environment-variable }}
          application_name: ${{ inputs.consumer-name }}
          pact_broker: ${{ env.PACT_BROKER_BASE_URL }}
          pact_broker_token: ${{ env.PACT_BROKER_TOKEN }}

  pact-record-deployment-consumer:
    needs: pact-can-i-deploy-consumer
    runs-on: ubuntu-latest
    steps:
      - uses: pactflow/actions/record-deployment@v1.0.0
        env:
          version: ${{ env.VERSION }}
          application_name: ${{ inputs.consumer-name }}
          environment: ${{ inputs.environment-variable }}

  pact-create-tag-consumer:
    needs: pact-record-deployment-consumer
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set Version App
        id: version
        working-directory: ./app/app_safi
        run: |
          export versionApp=$(yq eval '.version' pubspec.yaml)
          echo "::set-output name=version::$versionApp"
      - uses: pactflow/actions/create-version-tag@v1.0.0
        env:
          tag: ${{ steps.version.outputs.version }}
          version: ${{ env.VERSION }}
          environment: ${{ inputs.environment-variable }}
          application_name: ${{ inputs.consumer-name }}

  pact-publish-provider:
    needs: pact-publish-consumer
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository code
        uses: actions/checkout@v2
      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: "17"
          distribution: "temurin"
      - name: Build microservice
        working-directory: services/${{ inputs.microservice-name }}
        run: |
          ./gradlew clean build

  pact-can-i-deploy-provider:
    needs: pact-publish-provider
    runs-on: ubuntu-latest
    steps:
      - uses: pactflow/actions/can-i-deploy@v0.0.4
        env:
          to_environment: ${{ inputs.environment-variable }}
          application_name: ${{ inputs.provider-name }}
          pact_broker: ${{ env.PACT_BROKER_BASE_URL }}
          pact_broker_token: ${{ env.PACT_BROKER_TOKEN }}
          version: "0.1-SNAPSHOT"

  pact-record-deployment-provider:
    needs: pact-can-i-deploy-provider
    runs-on: ubuntu-latest
    steps:
      - uses: pactflow/actions/record-deployment@v1.0.0
        env:
          environment: ${{ inputs.environment-variable }}
          application_name: ${{ inputs.provider-name }}
          version: "0.1-SNAPSHOT"

  pact-create-tag-provider:
    needs: pact-record-deployment-provider
    runs-on: ubuntu-latest
    steps:
      - uses: pactflow/actions/create-version-tag@v1.0.0
        env:
          environment: ${{ inputs.environment-variable }}
          tag: ${{ inputs.environment-variable }}
          application_name: ${{ inputs.provider-name }}
          version: "0.1-SNAPSHOT"
safi-mobile-app-contract-test-ci.yml
name: Safi Mobile App Contract Test

on:
  workflow_call:
    inputs:
      environment-variable:
        required: true
        type: string

jobs:
  card-contract-test:
    name: Publish Card Contract to Pactflow
    uses: ./.github/workflows/safi-mobile-app-publish-contract-ci.yml
    with:
      environment-variable: ${{ inputs.environment-variable }}
      microservice-name: "card-manager"
      consumer-name: "safi-mobile-app"
      provider-name: "card-manager"

Example on Github Actions should look like this :

After the CI has been done, in the dashboard of Pactflow will look like this :

In a nutshell, if we want to visualize the flow it’s below.

Best Practices

  • Contract tests should focus on the messages (requests and responses) rather than the behavior.

  • Pact tests should be data-independent.

  • Use the broker to integrate Pact with your CI infrastructure.

  • Use pact for contract testing, not functional testing.

  • Only make assertions about the things that will affect the consumer if they change.

  • Make the latest pact available to the provider.

References :

  1. Introduction | Pact Docs

  2. Pactflow

  3. Contract Testing and PACT

  4. How To Publish Pact Contract To Pact Broker

  5. pact_dart 0.5.0

Attachments:

AwO0bVPwbGI0GNq29BhreODZfC_gNtbpRD0P_6qcrNWHRcCoY_fmS0uuPxbLsH4Qm5Q0F7t51Q_Go3WHKBpHwUF2ROY2GmzBY8kctSSK1REfZcjC1-Mmc_BXoRxrvvKLrmj3XCAXKIAbITTuD_msqIhfng3osyqeAIPx7Sy08OEm9j90cyzE81Q61w (image/png)
aBM1-wpTdN8cJ5ZJ4gO--K5ouezPGHWIAwSWXtHuIDoBvV-viTR2RFieLpdZ1j07nYQpR7_pfebUfAVb389pfhb6Kwlua-PXQ6MT1rTdfT1hKtAgxEJwj0_0kec-37Mp7mmSXKO9eamCGKNuPJOPErpJoN5g-6cq5dpgdbIb1WvUrhklJO_8n4dxFg (image/png)
0OLpdPGWa-4u3ica1T2ZxBQQNp89Qw91Y9bI8Ht4NJalr_I0E0XCW1WwA10jP9_DdRHS9NphUlpTbbv38LHn5rOkMmHO0DTuHpfipwR-d0yhBeMpy8Zkn8ix9Gwlwt7NsxpZnfo0wkc2hjLf2yugYI6aPmYw45NAT1TkTnXSsdMRnBjfPch_GxmA1A (image/png)
zdWp3LQ7MYr6d7mcQRQ5OlYNYM7reAUgFBePfTVnIKsDX_eYZmQ37zRraKMZLuOXV9vFn4EAAjINiyVyljeQ_ISy9dUUpozJBBcxg43Aabn2DODUFw5X97HCUqd76DfP4HCvbiQwDxHnk1DT-GeszViwKPcIyXw7Wb3elcm1wUjF_YyJ50nQkV5FQw (image/png)
Es0aI_etTm7yghb2oFIljTCWR01zln7ogBnqEaCGwc36HYZj3bqGyZI-P68_8x-vDsupfX7csXtfoI-Q71B5UTl0NWUUvQEJFw9HyvpfOr8enU1P_s6hJIMSZsX4pb31_3IQDH18SCJC_Bd7_OUn37h9KWi53gKVBo7J1vWKeFZPv2XOth1c8f02zg (image/png)
5oMW70q00VaTuKTViW7lfVxzS7PVQKdjxUFUCzqEiFHS1l6FK1pZzv-8eA-1VotG_ab2OAtvFV0bNpB-gua3o3lC6OlJyos4BeJXfkca69Wh8n8pyGg--hiRbXtOKYo7YbnjTrRCpyvxoSM95xATql2XeJ5xkF5Eh0jarwdCG1U6uBep_pfU-H5iNg (image/png)
ae7sUGOhOz9K-2QZX-RkZusL4i4G71AXR9YDpgLr72M1Kbydowi6VXQA4i0RqTBm42mLIIpllxGmyOouKUSLhYfLMxZGS8v_RyI2C56RwzpCMfqYQ7wvfvQA1FkuyvUSRgzc-EjwCFH5EtZE8Q0zQD3UHkylTkRLc2yW4O4oUFiXyaiIGjoO1qI8sw (image/png)
hB_MlO4fAqDke6On_ubKCpXU-Sm3UBGu-3L3NlM1nzMJRhc5I2eFXfA4JeRdG3jrjKhdkE2x37N6sYK_vvVyu-RNiQ56i-d1q3WM2HJR9JHgTLNQoLphoSxfru5wXqH9HkyJnAWxRvs0NnE_BcixZCv0XWMgw_XzmTPslu1oXaz6pnJLN7lnBV__9A (image/png)
ieMtu1sJJ1LbosfMMzthA9ymySH1Wy8e2EGSq7c7DB67izk_nh_lPI2Gtez3atFOlqIoppq1Mejcpj1vDfNfX2UUxnW2I5ufySq-5AOfc8FEh6GpHeGyFZCqPALo8TgtvejfkiD-jrYPb0scjICp4Q1Hrk1fHQhtLL0-qcWH_TWnUTXzzH8e_S3f7w (image/png)
EUXO5XGWsS9k9urMtCMM1rP5XfS7kuXNwIowwB8olHW4gxfVQM0j5gP6Pf-mzKzLCvm3i7DFi04Y-0FiIiPAvjUBWeaZTRaA0ySNSOq3PHnMpzPSObm-Wc8K2I-8t_MTJYm0i0Em5RyoLqObL2SyHUwBYvRbx2JLTo69uKufHJ1aff6PcFdeHbduYA (image/png)
364ZVjdEPo0OmI4cdMdeJ1npvLNSpv4_zfnKp59VaMA61rzx35ev-SBM681epQdqFkzG2p7NG05unqmU5eRrtsQj1FAjNBrKHvlvKmMJKe5zBldTBV7KdLKq1wjNtClKx6iJfYIS25bxE3HXZLXU618LdGGpD8TvHVsfTW-OaUcR3FF_bvpQFeuzLg (image/png)
8nAHODPOTFJUwpOhxBeDFVp036ZGA_oWJmgasZeqGpfb93ZJqQXxopI2CbT4jYF_TKdfz6cOrZ-yfHXfL6sy-ztSiulKrHc-0IHfiP6cNoZgvwAHnp4U3F84xtUhLn9mR2SQmm7IMSmoBSW3AJ6VuRFaiPVFQZz4kBJu_xWWxR4Hz8Tfvy852fxxig (image/png)
Screen Shot 2022-09-18 at 15.13.04.png (image/png)
5oMW70q00VaTuKTViW7lfVxzS7PVQKdjxUFUCzqEiFHS1l6FK1pZzv-8eA-1VotG_ab2OAtvFV0bNpB-gua3o3lC6OlJyos4BeJXfkca69Wh8n8pyGg--hiRbXtOKYo7YbnjTrRCpyvxoSM95xATql2XeJ5xkF5Eh0jarwdCG1U6uBep_pfU-H5iNg (image/png)
Es0aI_etTm7yghb2oFIljTCWR01zln7ogBnqEaCGwc36HYZj3bqGyZI-P68_8x-vDsupfX7csXtfoI-Q71B5UTl0NWUUvQEJFw9HyvpfOr8enU1P_s6hJIMSZsX4pb31_3IQDH18SCJC_Bd7_OUn37h9KWi53gKVBo7J1vWKeFZPv2XOth1c8f02zg (image/png)
6qvJjAqn2jlu8nAgjKoRYOpzVLAF2wNXl7MJCZPNwQrOmRwE9UFM0mwG1dUmu5XhFZsl1Sh18zw-49gtT5qgpbu8pc9tdCok7Hp7IW0F-gmnngv_AZnQC10JVt047zK8LyvxO1Ea5oqfb5an1ySASlBBmN0z9QA5-_4o1uSICt0rPaErM37gv2IH2fe7 (image/png)
4QUt3fLiddeatuOmgpMq-oYMumSsq0kZukvxlLxqIl5-t1CLHZasla1s2FSMxrsbuWgZ1OrXZ-VOuCGzM2GgRQUIvfaD5nBnHAZRtyiv_OWNB29mmJ3QTsICErxh7vEzBGvXJF1nfROPV5QFaj7vyUgQq7x4G16qnx1ldiNGGpoVFaSHrk0YR5jT9nm_ (image/png)
Screen Shot 2022-09-19 at 16.02.37.png (image/png)
image-20220919-090408.png (image/png)
Screen Shot 2022-12-06 at 15.49.14.png (image/png)
Screen Shot 2022-12-06 at 15.49.59.png (image/png)
Screen Shot 2022-12-06 at 15.50.21.png (image/png)
Screen Shot 2022-12-06 at 16.03.21.png (image/png)
Screen Shot 2022-12-06 at 16.04.09.png (image/png)
-894jTQ0Vl3usEaEbxNG-XVyY72BC0FrzBOFgtdP5cg4jrDlsjZsNoPr4I8hfLu334Ff4TLiv1HczwebdEgNGlU-Ynql-7N5DzRb_MgJDY-mpIuReyCbB8T10hg3YhaebeEOxIBNk-txQor-3_DZkVJkRO0SP03nr7vyvImbnwlpcrRL5D30LC0hdx1VwA (image/png)
Screen Shot 2022-12-08 at 08.56.10.png (image/png)
Screen Shot 2022-12-09 at 04.44.40.png (image/png)
Screen Shot 2022-12-09 at 04.48.06.png (image/png)
Screen Shot 2022-12-09 at 07.33.03.png (image/png)
Screen Shot 2022-12-09 at 07.37.58.png (image/png)
Screen Shot 2022-12-09 at 06.27.10.png (image/png)
Screen Shot 2022-12-09 at 07.38.28.png (image/png)
Screen Shot 2022-12-09 at 06.27.10.png (image/png)
Screen Shot 2022-12-09 at 08.31.33.png (image/png)