Skip to content

Commit

Permalink
Pz/types 004 - Add missing Customers API endpoints (#229)
Browse files Browse the repository at this point in the history
* changes to validateVoucher

* simple validations tests

* changelog

* Update tidy-comics-hear.md

* node 16.20

* node 16

* requested changes

* Update tidy-comics-hear.md

* done

* Update README.md

* Create customers.spec.ts

* Update Customers.ts

* Create tidy-ironman-two.md

* Update Customers.ts

* Update generateRandomString.ts

* requested changes

* Update package.json

* Update package.json

* Update tidy-comics-hear.md

* Revert "node 16.20"

This reverts commit f869c79.

* minor changes

* Delete createCampaignWithOnePromotionTierAndRandomName.ts

* requested changes

* docs

* Update tidy-ironman-two.md

* Revert "docs"

This reverts commit 4d1aa28.

* remove validations

* changeset

* Update tidy-comics-hear.md

* Update tidy-comics-hear.md

* Update README.md

* Update tidy-comics-hear.md

* Update tidy-comics-hear.md

* Revert "Update tidy-comics-hear.md"

This reverts commit 61bbc5a.

* Revert "Update tidy-comics-hear.md"

This reverts commit e6fd976.

* Update tidy-ironman-two.md

* Update tidy-comics-hear.md

* Create CHANGESET-TAMPLATE.md

* Update tidy-comics-hear.md

* Update CHANGESET-TAMPLATE.md

* Update CHANGESET-TAMPLATE.md

* Update CHANGESET-TAMPLATE.md

* Update CHANGESET-TAMPLATE.md

* Update tidy-comics-hear.md

* Update CHANGESET-TAMPLATE.md

* Update tidy-ironman-two.md

* Update tidy-ironman-two.md

* Update CHANGESET-TAMPLATE.md

* Update CHANGESET-TAMPLATE.md

* Update tidy-ironman-two.md

* dotenv in packages/sdk package.json

* requested changes

* requested changes

* requested changes

* Update tidy-comics-hear.md

* Delete tidy-comics-hear.md

* done

* Update ValidationsValidateVoucherResponse.html

* Update package.json

* update naming

* CustomersDeletePermanently

* typo

* typo

* revert permanent_deletion to pernament_deletion :)

* add birthdate to request

* guildeline

* delete `customers` from method

* add Customer building blocks

* align with original code from sdk (legacy)

* Update Customers.ts

* update changeset

* Update CONTRIBUTING.md

* fix description of domain types and CustomersUpdateInBulkRequestBody

* Update customers.spec.ts

* delete almost all properties from data_json (they could be different each time)

* add nullable to customer

* any -> uknown

---------

Co-authored-by: p-zielinski <[email protected]>
Co-authored-by: weronika-kurczyna <[email protected]>
Co-authored-by: Marcin Slezak <[email protected]>
  • Loading branch information
4 people authored Oct 9, 2023
1 parent de97b72 commit 5bb69da
Show file tree
Hide file tree
Showing 6 changed files with 521 additions and 2 deletions.
16 changes: 16 additions & 0 deletions .changeset/tidy-ironman-two.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
'@voucherify/sdk': minor
---

Add missing methods covering Customers API.
- Added support for new endpoints:
- `POST /customers/{customerId}/permanent-deletion`,
- `POST /customers/bulk/async`,
- `POST /customers/metadata/async` [(examples of usage available in readme.md)](..%2F..%2Fpackages%2Fsdk%2FREADME.md)
- New domain types:
- `CustomerBase`
- `CustomerAddress`
- New exported types/interfaces:
- `CustomersUpdateInBulkRequestBody`,
- `CustomersUpdateMetadataInBulkRequestBody`,
- `CustomersDeletePermanentlyResponseBody`
252 changes: 252 additions & 0 deletions packages/sdk/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@

## Introduction

By its nature, SDK should help software developers integrate with Voucherify REST API. In most cases, Voucherify SDK methods are just wrappers for HTTP clients, so the SDK values that can help a software developer are:
- types for requests and responses
- examples of use
- documentation describing the endpoints

Precisely defined and well-designed request and response types are the main value of the Voucherify SDK, especially since our REST API uses large and complex data structures for communication. We should pay attention to the following:
- aligned SDK with OpenAPI Specification and REST API (naming, data structures)
- covering in SDK all available endpoints in REST API
- covering in types all available properties in data
- types names and structure that is intuitive for developers

This document describes good practices and patterns we should use in developing Voucherify SDK in correlation with Voucherify OpenAPI specification and [Voucherify OpenAPI Guideline](https://github.com/voucherifyio/voucherify-openapi/blob/master/CONTRIBUTING.md), including strategy for legacy code and backward compatibility.


## Types structure

We can recognise two groups of types in SDK:
- **0-level Types**, used in SDK methods, directly describe the parameters and returned values.
- **Domain Types**, describing fundamental domain building blocks used in Voucherify.

0-level Types can use Domain Types, not the other way around.

## 0-level Types

In most cases, Voucherify SDK methods are just wrappers for HTTP clients, so 0-level Types should be aligned to the Voucherify OpenAPI file in the context of:
- resource naming
- types naming
- data structure, property types, required/optional attribute


Because of that, the 0-level Types naming should be aligned with [OpenAPI Types](https://github.com/voucherifyio/voucherify-openapi/blob/master/CONTRIBUTING.md#openapi), but instead of snake casing, we will use the pascal case casing used in SDK.

Pattern: `<resource><action><request|response><body|param|query>`, where
`resource`: plural name taken from API path, e.g. `vouchers``customers``products` 
`action` : `get`(single record), `list``update``delete``create` (etc.)
- `request` or `response`
- `body`, `param` or `query`

If in 0-level Types, there is a need to refer to created child types (enums, list items, other large structures), and those child types are used only for this one 0-level, we can still name them as 0-level type, including additional information after `action` and following the pattern:

`<resource><action><child type distinction><request|response><body|param|query>`

For example:

The 0-level type that describes the voucher list response body is named `VouchersListResponseBody`, but the voucher object is large itself; it's also a good practice to give the SDK user the type of the specific list item so he can use it easily in his application, so we should create the type describing voucher object and name it: `VouchersListItemResponseBody`.

## Domain Types

Domain Types:
- should not be identified with any specific API endpoint or SDK method
- should prefer composition over inherence
- names should describe what it's inside, not where it's used, e.g: `CustomerAddress`, `CustomerSummary`
- should have optional fields; we can mark them as required in 0-level Types
- avoid utility types like: Pick, Omit, Partial

> [!NOTE] Do not create domain type if we can't give intuitive name
## Examples
> [!WARNING] This example shows how we can structure the types, it's not Voucherify specification
```ts
// Utility types 👇

// Mark specific property as required
type WithRequiredProperty<Type, Key extends keyof Type> = Type & {
[Property in Key]-?: Type[Property];
};
// Remove null as the option type for specific properties
type WithNonNullableProperty<Type, Key extends keyof Type> = Type & {
[Property in Key]: Exclude<Type[Property], null> ;
};

// Mark all nested properties as required
// Use DeepRequired<> from `utility-types` - https://www.npmjs.com/package/utility-types#deeprequiredt

// Domain types 👇

// Basic customer properties, common for most of the 0-level endpoints
type CustomerBase = {
name?: string
email?: string
phone?: string
description?: string
birthdate?: string
metadata?: Record<string, any>
}

type CustomerUnconfirmed = CustomerLoyalty
& CustomerSummary
& {
email?: string
object: 'unconfirmed_customer'
}

// Can we use it as pattern in other places?
type CustomerIdentity = {
id?: string
source_id? :string
}


type CustomerAddress = {
address?: {
city?: string | null
state?: string | null
line_1?: string | null
line_2?: string | null
country?: string | null
postal_code?: string | null
} | null
}

type CustomerSummary = {
summary: {
redemptions: {
total_redeemed: number
total_failed: number
total_succeeded: number
total_rolled_back: number
total_rollback_failed: number
total_rollback_succeeded: number
gift?: {
redeemed_amount: number
amount_to_go: number
}
loyalty?: {
redeemed_points: number
points_to_go: number
}
}
orders: {
total_amount: number
total_count: number
average_amount: number
last_order_amount: number
last_order_date?: string
}
}
}

type CustomerReferals = {
referrals: {
campaing_id: string
referrer_id: string
related_object_id: string
related_object_type: string
date: string
}
}

type CustomerLoyalty = {
loyalty: {
points: number
referred_customers: number
campaigns?: Record<
string,
{
points: number
referred_customers: number
}
>
}
}

type CustomerSaved = {
object: 'customer'
system_metadata: {
consent: {
consentId: string
}
}
created_at: string
updated_at: string
assets: {
cocpit_url: string
}
}

// Exists because:
// - it's defined as building block in Voucherify documentation: https://docs.voucherify.io/reference/customer-object
// - it's used in two places: List item in response and get response
type Customer = Required<CustomerBase>
& Required<CustomerIdentity>
& Required<CustomerAddress>
& Required<CustomerSummary>
& Required<CustomerLoyalty>
& CustomerReferals
& CustomerSaved

// 0 Level types 👇

type CustomersGetResponseBody = Customer

type CustomerListRequestParams = {
limit?: number
page?: number
email?: string
city?: string
name?: string
order?: string
starting_after?: string
}

type CustomersListResponseBody = {
object: 'list'
data_ref: 'customers'
customers: (Customer)[]
total: number
has_more: boolean
}

type CustomerCreateRequestBody = WithRequiredProperty<CustomerBase, 'email' | 'name' > // an example how to mark specific fields as required
& CustomerAddress
& {
source_id?: string
}

type CustomerUpdateRequestBody = CustomerBase

type IdOrSourceId = { id: string } | { source_id: string }
type CustomerCreateResponseBody = Customer;


customer.get(id: string): CustomersGetResponseBody{}
customer.list(CustomerListRequestParams):CustomerListResponseBody {}
customer.create(customer: CustomerCreateRequestBody): CustomerCreateResponseBody{}
customer.delete(id: string): CustomerDeleteResponseBody{}
customre.update(customer: CustomerUpdateRequestBody): CustomerUpdateResponseBody{}

```



## Legacy

The existing SDK doesn't have one consistent structure for types in the context of naming or structure. Working on SDK, we could adjust all legacy code to the new guideline, but it will generate many non-backwards compatible changes that we want to avoid as long as possible because it will require SDK users to adjust their existing integrations. On the other hand, we can not be blocked to improve SDK at the end. We will split the work into two phases:
1. Phase one: pick low-hanging fruits by adding missing attributes to the existing types in a backwards-compatible manner and adding support for the new API Endpoints, where we can create a new type aligned to recent guidelines.
2. Phase two: cumulative major version bumps, where we can replace legacy types with new ones.

We split the types from the file into two groups and described them by comments.

```ts
// Legacy types 👇
export type CustomerReq = { ... }

// Types following the Guideline: https:// .... 👇


```
25 changes: 23 additions & 2 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ const client = VoucherifyServerSide({
apiUrl: 'https://<region>.api.voucherify.io', // optional
apiVersion: 'v2018-08-01', // optional
channel: 'e-commerce', // optional
customHeaders: { "MY_CUSTOM_HEADER": "my_value" } // optional
customHeaders: { "MY_CUSTOM_HEADER": "my_value" }, // optional
timeoutMs: 10000 // optional
})
```
Expand Down Expand Up @@ -574,7 +574,7 @@ Check [promotion's tier object](https://docs.voucherify.io/reference/the-promoti

```javascript
client.promotions.tiers.listAll()
client.promotions.tiers.listAll(params})
client.promotions.tiers.listAll(params)
```

#### [Add Promotion Tier to Campaign](https://docs.voucherify.io/reference/add-promotion-tier-to-campaign)
Expand Down Expand Up @@ -611,9 +611,12 @@ Methods are provided within `client.customers.*` namespace.
- [Get Customer](#get-customer)
- [Update Customer](#update-customer)
- [Delete Customer](#delete-customer)
- [Delete Customer Permanently](#delete-customer-permanently)
- [List Customers](#list-customers)
- [Update Customer's Consents](#update-customers-consents)
- [List Customer's Activities](#list-customers-activities)
- [Update Customers in bulk](#update-customers-in-bulk)
- [Update Customers' Metadata in bulk](#update-customers-metadata-in-bulk)
- [Import and Update Customers using CSV](#import-and-update-customers-using-csv)

#### [Create Customer](https://docs.voucherify.io/reference/create-customer)
Expand Down Expand Up @@ -644,6 +647,12 @@ client.customers.update(customer)
client.customers.delete(customerId)
```

#### [Delete Customer Permanently](https://docs.voucherify.io/reference/delete-customer-permanently)

```javascript
client.customers.deletePermanently(customerId)
```

#### [List Customers](https://docs.voucherify.io/reference/list-customers)

```javascript
Expand Down Expand Up @@ -702,6 +711,18 @@ client.customers.listActivities(customerId, params)
client.customers.importCSV(filePath)
```

#### [Update Customers in bulk](https://docs.voucherify.io/reference/update-customers-in-bulk)

```javascript
client.customers.updateInBulk(customers)
```

#### [Update Customers' Metadata in bulk](https://docs.voucherify.io/reference/update-customers-metadata-in-bulk)

```javascript
client.customers.updateMetadataInBulk(sourceIdsAndMetadata)
```

---

### Consents
Expand Down
21 changes: 21 additions & 0 deletions packages/sdk/src/Customers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,33 @@ class Customers {

return this.client.put<T.CustomersUpdateResponse>(`/customers/${encode(id)}`, customerWithoutId)
}
/**
* @see https://docs.voucherify.io/reference/update-customers-in-bulk
*/
public updateInBulk(customers: T.CustomersUpdateInBulkRequestBody) {
return this.client.post<AAT.AsyncActionCreateResponse>(`/customers/bulk/async`, customers)
}
/**
* @see https://docs.voucherify.io/reference/update-customers-metadata-in-bulk
*/
public updateMetadataInBulk(sourceIdsAndMetadata: T.CustomersUpdateMetadataInBulkRequestBody) {
return this.client.post<AAT.AsyncActionCreateResponse>(`/customers/metadata/async`, sourceIdsAndMetadata)
}
/**
* @see https://docs.voucherify.io/reference/delete-customer
*/
public delete(customerId: string) {
return this.client.delete<undefined>(`/customers/${encode(customerId)}`)
}
/**
* @see https://docs.voucherify.io/reference/delete-customer-permanently
*/
public deletePermanently(customerId: string) {
return this.client.post<T.CustomersDeletePermanentlyResponseBody>(
`/customers/${encode(customerId)}/permanent-deletion`,
{},
)
}
/**
* @see https://docs.voucherify.io/reference/update-customers-consents
*/
Expand Down
Loading

0 comments on commit 5bb69da

Please sign in to comment.