Browse Source

Merge branch 'minor' into major

Michael Bromley 2 years ago
parent
commit
c76ad29f13

+ 1 - 1
.github/CODE_OF_CONDUCT.md

@@ -1,7 +1,7 @@
 # Vendure Code of Conduct
 
 ## Scope
-This code of conduct applies to those who wish to contribute to the Vendure project (any repo under the [vendure-ecommerce](https://github.com/vendure-ecommerce) organization) by way of issues and pull requests. It also applies to official project communications channels such as Slack.
+This code of conduct applies to those who wish to contribute to the Vendure project (any repo under the [vendure-ecommerce](https://github.com/vendure-ecommerce) organization) by way of issues and pull requests. It also applies to official project communications channels such as Discord.
 
 ## Standards
 

+ 24 - 0
CHANGELOG.md

@@ -1,3 +1,27 @@
+## <small>1.9.7 (2023-05-19)</small>
+
+#### Fixes
+
+* **create** Update community link to point to new Discord server
+
+#### Features
+
+* **payments-plugin** Add metadata field to StripePluginOptions (#2157) ([21baa0a](https://github.com/vendure-ecommerce/vendure/commit/21baa0a)), closes [#2157](https://github.com/vendure-ecommerce/vendure/issues/2157) [#1935](https://github.com/vendure-ecommerce/vendure/issues/1935)
+
+## <small>1.9.6 (2023-04-28)</small>
+
+
+#### Fixes
+
+* **admin-ui** Add branding to welcome page (#2115) ([f0f8769](https://github.com/vendure-ecommerce/vendure/commit/f0f8769)), closes [#2115](https://github.com/vendure-ecommerce/vendure/issues/2115) [#2040](https://github.com/vendure-ecommerce/vendure/issues/2040)
+* **asset-server-plugin** Change image format with no other transforms (#2104) ([6cf1608](https://github.com/vendure-ecommerce/vendure/commit/6cf1608)), closes [#2104](https://github.com/vendure-ecommerce/vendure/issues/2104)
+* **core** Fix error messages containing colon char ([2cfc874](https://github.com/vendure-ecommerce/vendure/commit/2cfc874)), closes [#2153](https://github.com/vendure-ecommerce/vendure/issues/2153)
+
+#### Features
+
+* **admin-ui** Implement custom fields updating of ProductOptionGroup and ProductOption entities ([d2a0824](https://github.com/vendure-ecommerce/vendure/commit/d2a0824))
+* **admin-ui** Search field added on administrators list on dashboard -> administrator. (#2130) ([0cc20f2](https://github.com/vendure-ecommerce/vendure/commit/0cc20f2)), closes [#2130](https://github.com/vendure-ecommerce/vendure/issues/2130)
+
 ## <small>1.9.5 (2023-03-24)</small>
 
 #### Fixes 

+ 4 - 1
README.md

@@ -10,7 +10,7 @@ A headless [GraphQL](https://graphql.org/) ecommerce framework built on [Node.js
 
 * [Getting Started](https://www.vendure.io/docs/getting-started/): Get Vendure up and running locally in a matter of minutes with a single command
 * [Live Demo](https://demo.vendure.io/)
-* [Vendure Slack](https://www.vendure.io/slack) Join us on Slack for support and answers to your questions
+* [Vendure Discord](https://www.vendure.io/community) Join us on Discord for support and answers to your questions
 
 ## Structure
 
@@ -41,6 +41,9 @@ The root directory has a `package.json` which contains build-related dependencie
 * Generating TypeScript types from the GraphQL schema
 * Linting, formatting & testing tasks to run on git commit & push
 
+> Note:
+> When you do `yarn` for the first time, you will need to manually create the `package` folder under [/packages/admin-ui](/packages/admin-ui).
+
 ### 2. Bootstrap the packages
 
 `yarn bootstrap`

+ 1 - 1
packages/create/templates/readme.hbs

@@ -5,7 +5,7 @@ This project was generated with [`@vendure/create`](https://github.com/vendure-e
 Useful links:
 
 - [Vendure docs](https://www.vendure.io/docs)
-- [Vendure Slack community](https://www.vendure.io/slack)
+- [Vendure Discord community](https://www.vendure.io/community)
 - [Vendure on GitHub](https://github.com/vendure-ecommerce/vendure)
 - [Vendure plugin template](https://github.com/vendure-ecommerce/plugin-template)
 

+ 39 - 0
packages/payments-plugin/e2e/stripe-metadata-sanitize.e2e-spec.ts

@@ -0,0 +1,39 @@
+import { sanitizeMetadata } from '../src/stripe/metadata-sanitize';
+
+describe('Stripe Metadata Sanitize', () => {
+    const metadata = {
+        customerEmail: 'test@gmail.com',
+    };
+    it('should sanitize and create new object metadata', () => {
+        const newMetadata = sanitizeMetadata(metadata);
+        expect(newMetadata).toEqual(metadata);
+        expect(newMetadata).not.toBe(metadata);
+    });
+    it('should omit fields that have key length exceed 40 characters', () => {
+        const newMetadata = sanitizeMetadata({
+            ...metadata,
+            reallylongkey_reallylongkey_reallylongkey_reallylongkey_reallylongkey: 1,
+        });
+        expect(newMetadata).toEqual(metadata);
+    });
+    it('should omit fields that have value length exceed 500 characters', () => {
+        const reallyLongText = Array(501).fill('a').join();
+        const newMetadata = sanitizeMetadata({
+            ...metadata,
+            complexField: reallyLongText,
+        });
+        expect(newMetadata).toEqual(metadata);
+    });
+    it('should truncate metadata that have more than 50 keys', () => {
+        const moreThan50KeysMetadata = Array(51)
+            .fill('a')
+            .reduce((obj, val, idx) => {
+                obj[idx] = val;
+                return obj;
+            }, {});
+        const newMetadata = sanitizeMetadata(moreThan50KeysMetadata);
+        expect(Object.keys(newMetadata).length).toEqual(50);
+        delete moreThan50KeysMetadata['50'];
+        expect(newMetadata).toEqual(moreThan50KeysMetadata);
+    });
+});

+ 35 - 1
packages/payments-plugin/e2e/stripe-payment.e2e-spec.ts

@@ -1,5 +1,5 @@
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
-import { mergeConfig } from '@vendure/core';
+import { EntityHydrator, mergeConfig } from '@vendure/core';
 import {
     CreateProductMutation,
     CreateProductMutationVariables,
@@ -176,6 +176,40 @@ describe('Stripe payments', () => {
         expect(createStripePaymentIntent).toEqual('test-client-secret');
     });
 
+    // https://github.com/vendure-ecommerce/vendure/issues/1935
+    it('should attach metadata to stripe payment intent', async () => {
+        StripePlugin.options.metadata = async (injector, ctx, currentOrder) => {
+            const hydrator = await injector.get(EntityHydrator);
+            await hydrator.hydrate(ctx, currentOrder, { relations: ['customer'] });
+            return {
+                customerEmail: currentOrder.customer?.emailAddress ?? 'demo',
+            };
+        };
+        let createPaymentIntentPayload: any;
+        const { activeOrder } = await shopClient.query<GetActiveOrderQuery>(GET_ACTIVE_ORDER);
+        nock('https://api.stripe.com/')
+            .post('/v1/payment_intents', body => {
+                createPaymentIntentPayload = body;
+                return true;
+            })
+            .reply(200, {
+                client_secret: 'test-client-secret',
+            });
+        const { createStripePaymentIntent } = await shopClient.query(CREATE_STRIPE_PAYMENT_INTENT);
+        expect(createPaymentIntentPayload).toEqual({
+            amount: activeOrder?.totalWithTax.toString(),
+            currency: activeOrder?.currencyCode?.toLowerCase(),
+            customer: 'new-customer-id',
+            'automatic_payment_methods[enabled]': 'true',
+            'metadata[channelToken]': E2E_DEFAULT_CHANNEL_TOKEN,
+            'metadata[orderId]': '1',
+            'metadata[orderCode]': activeOrder?.code,
+            'metadata[customerEmail]': customers[0].emailAddress,
+        });
+        expect(createStripePaymentIntent).toEqual('test-client-secret');
+        StripePlugin.options.metadata = undefined;
+    });
+
     // https://github.com/vendure-ecommerce/vendure/issues/1630
     describe('currencies with no fractional units', () => {
         let japanProductId: string;

+ 35 - 0
packages/payments-plugin/src/stripe/metadata-sanitize.ts

@@ -0,0 +1,35 @@
+import Stripe from 'stripe';
+
+const MAX_KEYS = 50;
+const MAX_KEY_NAME_LENGTH = 40;
+const MAX_VALUE_LENGTH = 500;
+/**
+ * @description
+ * Santitize metadata to ensure it follow Stripe's instructions
+ *
+ * @link
+ * https://stripe.com/docs/api/metadata
+ *
+ * @Restriction
+ * You can specify up to 50 keys, with key names up to 40 characters long and values up to 500 characters long.
+ *
+ */
+export function sanitizeMetadata(metadata: Stripe.MetadataParam) {
+    if (typeof metadata !== 'object' && metadata !== null) return {};
+
+    const keys = Object.keys(metadata)
+        .filter(keyName => keyName.length <= MAX_KEY_NAME_LENGTH)
+        .filter(
+            keyName =>
+                typeof metadata[keyName] !== 'string' ||
+                (metadata[keyName] as string).length <= MAX_VALUE_LENGTH,
+        )
+        .slice(0, MAX_KEYS) as Array<keyof Stripe.MetadataParam>;
+
+    const sanitizedMetadata = keys.reduce((obj, keyName) => {
+        obj[keyName] = metadata[keyName];
+        return obj;
+    }, {} as Stripe.MetadataParam);
+
+    return sanitizedMetadata;
+}

+ 15 - 5
packages/payments-plugin/src/stripe/stripe.service.ts

@@ -1,8 +1,10 @@
 import { Inject, Injectable } from '@nestjs/common';
+import { ModuleRef } from '@nestjs/core';
 import { ConfigArg } from '@vendure/common/lib/generated-types';
 import {
     Ctx,
     Customer,
+    Injector,
     Logger,
     Order,
     Payment,
@@ -14,6 +16,7 @@ import {
 import Stripe from 'stripe';
 
 import { loggerCtx, STRIPE_PLUGIN_OPTIONS } from './constants';
+import { sanitizeMetadata } from './metadata-sanitize';
 import { VendureStripeClient } from './stripe-client';
 import { getAmountInStripeMinorUnits } from './stripe-utils';
 import { stripePaymentMethodHandler } from './stripe.handler';
@@ -25,6 +28,7 @@ export class StripeService {
         private connection: TransactionalConnection,
         private paymentMethodService: PaymentMethodService,
         @Inject(STRIPE_PLUGIN_OPTIONS) private options: StripePluginOptions,
+        private moduleRef: ModuleRef,
     ) {}
 
     async createPaymentIntent(ctx: RequestContext, order: Order): Promise<string> {
@@ -35,6 +39,16 @@ export class StripeService {
             customerId = await this.getStripeCustomerId(ctx, order);
         }
         const amountInMinorUnits = getAmountInStripeMinorUnits(order);
+
+        const metadata = sanitizeMetadata({
+            ...(typeof this.options.metadata === 'function'
+                ? await this.options.metadata(new Injector(this.moduleRef), ctx, order)
+                : {}),
+            channelToken: ctx.channel.token,
+            orderId: order.id,
+            orderCode: order.code,
+        });
+
         const { client_secret } = await stripe.paymentIntents.create(
             {
                 amount: amountInMinorUnits,
@@ -43,11 +57,7 @@ export class StripeService {
                 automatic_payment_methods: {
                     enabled: true,
                 },
-                metadata: {
-                    channelToken: ctx.channel.token,
-                    orderId: order.id,
-                    orderCode: order.code,
-                },
+                metadata,
             },
             { idempotencyKey: `${order.code}_${amountInMinorUnits}` },
         );

+ 14 - 0
packages/payments-plugin/src/stripe/types.ts

@@ -1,5 +1,7 @@
+import { Injector, Order, RequestContext } from '@vendure/core';
 import '@vendure/core/dist/entity/custom-entity-fields';
 import { Request } from 'express';
+import Stripe from 'stripe';
 
 // Note: deep import is necessary here because CustomCustomerFields is also extended in the Braintree
 // plugin. Reference: https://github.com/microsoft/TypeScript/issues/46617
@@ -27,6 +29,18 @@ export interface StripePluginOptions {
      * @default false
      */
     storeCustomersInStripe?: boolean;
+
+    /**
+     * @description
+     * Attach extra metadata to Stripe payment intent
+     *
+     * @since 1.9.7
+     */
+    metadata?: (
+        injector: Injector,
+        ctx: RequestContext,
+        order: Order,
+    ) => Stripe.MetadataParam | Promise<Stripe.MetadataParam>;
 }
 
 export interface RequestWithRawBody extends Request {