vuestorefront / vue-storefront

Alokai is a Frontend as a Service solution that simplifies composable commerce. It connects all the technologies needed to build and deploy fast & scalable ecommerce frontends. It guides merchants to deliver exceptional customer experiences quickly and easily.
https://www.alokai.com
MIT License
10.55k stars 2.08k forks source link

Refactor of the Payments API #5830

Closed Fifciu closed 2 years ago

Fifciu commented 3 years ago

What is the motivation for adding / enhancing this feature?

What are the acceptance criteria

Assumptions

PSP - Payment service provider(adyen, checkout.com).

Implementation of the Adapters

a) new fields in middleware.config.js:

adyen: {
    location: '@vsf-enterprise/adyen/server',
    configuration: {
      adapter: ['commercetools', { // Rememberfirst arg of this array!
        apiHost: 'https://api.europe-west1.gcp.commercetools.com',
        authHost: 'https://auth.europe-west1.gcp.commercetools.com',
        projectKey: 'vsf-ct-dev',
        clientId: 't5fqo0O942BeTndxHscAf5b0',
        clientSecret: 'lJJw-OkECeWsQHuemVLf4LeAFU1cGIM3',
        scopes: [
          'manage_orders:vsf-ct-dev',
          'manage_payments:vsf-ct-dev'
        ]
      }],
      api: {
        adyenMerchantAccount: 'VSFAccount243ECOM',
        origin: 'http://localhost:3000'
      },
      buildRedirectUrlAfter3ds1Auth (paymentAndOrder, succeed) {
        let redirectUrl = `/checkout/thank-you?order=${paymentAndOrder.order.id}`;
        if (!succeed) {
          redirectUrl += '&error=authorization-failed';
        }
        return redirectUrl;
      },
      buildRedirectUrlAfter3ds1Error (err) {
        return '/?3ds1-server-error';
      }
    }
  }

b) Then in index.server.ts which is entry point for our middleware integration:

const { createApiClient } = apiClientFactory<AdyenConfig, AdyenAdapterMethods>({
  onCreate: (config: AdyenConfig) => {
    const client = buildApiClient(...);

    return {
      config,
      client
    }
  },
  api: async (config: AdyenConfig): Promise<AdyenAdapterMethods> => { 
    // IT IS IMPOSSIBLE RIGHT NOW
    // Currently, it can be only an object. 
    try {
      const adapter = await import(`@vsf-enterprise/adyen-${config.adapter[0]}`);
      return adapter
    } catch (err) {
      throw new Error(`${config.adapter[0]} adapter for Adyen is not implemented.`)
    }
  }
});

Here we have to adjust api part of apiClientFactory. We should be able to make it aysnc method that returns API methods.

Questions

  1. Can we add this functionality to the api property?
  2. One monorepo for payments vs One monorepo per PSP?

Version of Vue Storefront

Can you complete this feature request by yourself?

Additional information

Untitled Diagram (2)

sync-by-unito[bot] commented 3 years ago

➤ Filip Jędrasik commented:

https://github.com/vuestorefront/vue-storefront/issues/5830 ( https://github.com/vuestorefront/vue-storefront/issues/5830|smart-link )

filrak commented 3 years ago

I like it but i see a room for improvement in terms of adapters api.

The adapter itself is a set of functions and properties, so it shouldn't be an additional field in the middleware config but a single object. Otherwise, it would be hard to externalize it into a separate package.

I would see that more as something like this:


const commerceToolsAdapter = {
  buildRedirectUrlAfter3ds1Auth (paymentAndOrder, succeed) {
        let redirectUrl = `/checkout/thank-you?order=${paymentAndOrder.order.id}`;
        if (!succeed) {
          redirectUrl += '&error=authorization-failed';
        }
        return redirectUrl;
      },
      buildRedirectUrlAfter3ds1Error (err) {
        return '/?3ds1-server-error';
      }
}

// in config
adyen: {
    location: '@vsf-enterprise/adyen/server',
    configuration: {
      adapter: commerceToolsAdapter,
      ct: {
        apiHost: 'https://api.europe-west1.gcp.commercetools.com',
        authHost: 'https://auth.europe-west1.gcp.commercetools.com',
        projectKey: 'vsf-ct-dev',
        clientId: 't5fqo0O942BeTndxHscAf5b0',
        clientSecret: 'lJJw-OkECeWsQHuemVLf4LeAFU1cGIM3',
        scopes: [
          'manage_orders:vsf-ct-dev',
          'manage_payments:vsf-ct-dev'
        ]
      },
      api: {
        adyenMerchantAccount: 'VSFAccount243ECOM',
        origin: 'http://localhost:3000'
      },
    }
  }

Whole adapter should be a single object. It could be either part of a config or an extension

Also regarding API client part you shouldn't made assumptions about the structure/naming of the adapter package, by doing this you're just increasing complexity and API surface to maintain (potential problems when you want to change sth in the future + unnecessary limitation preventing others from writing their adapters)

andrzejewsky commented 3 years ago

@filrak I think you didn't understand the context

adyen: {
  location: '@vsf-enterprise/adyen/server',
  configuration: {
    adapter: [
      // this is the name of package with adapter
      '@vsf-enterprise/adyen-commercetools',
      { 

        // below you have configuration for this adapter:
        // in this case it's commercetools so you need to define connection again
        apiHost: 'https://api.europe-west1.gcp.commercetools.com',
        authHost: 'https://auth.europe-west1.gcp.commercetools.com',
        projectKey: 'vsf-ct-dev',
        clientId: 't5fqo0O942BeTndxHscAf5b0',
        clientSecret: 'lJJw-OkECeWsQHuemVLf4LeAFU1cGIM3',
        scopes: [
          'manage_orders:vsf-ct-dev',
          'manage_payments:vsf-ct-dev'
        ]
      }
    ],
    // here is the section for configuration the payment integration itself:
    api: {
        adyenMerchantAccount: 'VSFAccount243ECOM',
        origin: 'http://localhost:3000'
    }
  }
}

So the adapter section is an array (the first element is a package name, second configuration), when you want to configure the adapter or just a string with package name if it doesn't have the configuration - this is the same way how nuxt.js works with its modules.

Now adapters. The single adapter is a function (not object), a function that takes the configuration (from above) and then returns an object.

const commercetoolsAdapter = (config) => {
  const connection = createConnection(config)

  return {
    someFunction1: () => {
      connection.something()
    },
    someFunction2: () => {
      connection.something()
    },
  }
}
Fifciu commented 3 years ago
// Core Type
type PaymentAdapter<C,E> = (config: C) => E;

// Main Adyen Package
interface AdyenEndpoints {
  getPaymentMethodsRequest: Function,
  makePaymentRequest: Function,
  submitAdditionalPaymentDetailsRequest: Function,
  cardAuthAfterRedirect: Function,
}
interface AdyenConfig {
  // ...
}

export type AdyenAdapter = PaymentAdapter<AdyenConfig, AdyenEndpoints>;

// Commercetools/M2/Other adapter
import { AdyenAdapter } from '@vsf-enterprise/adyen';

So commercetoolsAdapter from Andrzej's example should be type AdyenAdapter

andrzejewsky commented 3 years ago

@Fifciu if you are sure that, you will always return functions, would be nice to describe it using TS interfaces

Playground link

Additionally, you can include that in the first argument the context is accesible

filipsobol commented 2 years ago

Closing as agreed with @Fifciu