feat: medusa-source-shopify loader (#563)

* added statuses to product + unit test for updating status

* add update to product model

* added integration tests

* added integration test to validate that updating status to null results in invalid_data error

* removed comment

* update GET /store/products integration test

* fixed unit test with IdMap

* init plugin

* changed dbehaviour on invalid status input on admin list products

* mprices

* updated migration to add status = published on all existing products + added integration test on GET /admin/products when status null is provided

* merged product status

* init ShopifyService

* made requested changes to migration and GET /store/products

* fixed test

* made requested changes to migration

* push progress on source plugin

* add webhook product/create handler

* fixed normalization of variant weight

* removed weight func

* work on events

* finished product hooks (error on new variant needs to be fixed)

* fixed order status

* create fulfillments

* update fulfillment on cancel

* refactored services, handle returns though medusa, helper methods

* order updates

* removed dist

* update gitignore

* emit cahnges to product

* added redis ignore check to prevent update loops

* fixed product-variant.deleted event

* fix more events

* fix test

* fix: order taxes

* added refund with no items

* fixes to hooks

* fixed handling refunds and returns issued from Shopify

* added unit tests to ShopifyProductService and ShopifyCollectionService

* linting fix

* prepared loader PR

* fix: jsDocs

* fix: pager

* fix: build output and babelrc

* chore: linting

* fix: address type

* fix: migration clean up

* fix: update snapshots with ext_ids

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>
This commit is contained in:
Kasper Fabricius KristensenandSebastian Rindom authored and GitHub committed 2021-12-08 10:09:21 +01:00
1 parent f2ba4018fc
commit 577bcc23d4
46 files changed
+7857 -61

No files matched your search

@@ -0,0 +1,19 @@
import { MigrationInterface, QueryRunner } from "typeorm"
export class externalIdOrder1638952072999 implements MigrationInterface {
name = "externalIdOrder1638952072999"
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "order" ADD "external_id" character varying`
)
await queryRunner.query(
`ALTER TABLE "product" ADD "external_id" character varying`
)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "product" DROP COLUMN "external_id"`)
await queryRunner.query(`ALTER TABLE "order" DROP COLUMN "external_id"`)
}
}
+11 -11
View File
@@ -50,45 +50,45 @@ export class Address {
id: string
@Index()
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
customer_id: string | null
@ManyToOne(() => Customer)
@JoinColumn({ name: "customer_id" })
customer: Customer | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
company: string | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
first_name: string | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
last_name: string | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
address_1: string | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
address_2: string | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
city: string | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
country_code: string | null
@ManyToOne(() => Country)
@JoinColumn({ name: "country_code", referencedColumnName: "iso_2" })
country: Country | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
province: string | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
postal_code: string | null
@Column({ type: "text", nullable: true })
@Column({ type: "varchar", nullable: true })
phone: string | null
@CreateDateColumn({ type: resolveDbType("timestamptz") })
+12 -9
View File
@@ -175,7 +175,7 @@ export class Order {
@OneToMany(
() => ShippingMethod,
method => method.order,
(method) => method.order,
{
cascade: ["insert"],
}
@@ -184,14 +184,14 @@ export class Order {
@OneToMany(
() => Payment,
payment => payment.order,
(payment) => payment.order,
{ cascade: ["insert"] }
)
payments: Payment[]
@OneToMany(
() => Fulfillment,
fulfillment => fulfillment.order,
(fulfillment) => fulfillment.order,
{
cascade: ["insert"],
}
@@ -200,28 +200,28 @@ export class Order {
@OneToMany(
() => Return,
ret => ret.order,
(ret) => ret.order,
{ cascade: ["insert"] }
)
returns: Return[]
@OneToMany(
() => ClaimOrder,
co => co.order,
(co) => co.order,
{ cascade: ["insert"] }
)
claims: ClaimOrder[]
@OneToMany(
() => Refund,
ref => ref.order,
(ref) => ref.order,
{ cascade: ["insert"] }
)
refunds: Refund[]
@OneToMany(
() => Swap,
swap => swap.order,
(swap) => swap.order,
{ cascade: ["insert"] }
)
swaps: Swap[]
@@ -235,7 +235,7 @@ export class Order {
@OneToMany(
() => LineItem,
lineItem => lineItem.order,
(lineItem) => lineItem.order,
{
cascade: ["insert"],
}
@@ -244,7 +244,7 @@ export class Order {
@OneToMany(
() => GiftCardTransaction,
gc => gc.order
(gc) => gc.order
)
gift_card_transactions: GiftCardTransaction[]
@@ -266,6 +266,9 @@ export class Order {
@Column({ nullable: true })
idempotency_key: string
@Column({ type: "varchar", nullable: true })
external_id: string | null
// Total fields
shipping_total: number
discount_total: number
+3
View File
@@ -150,6 +150,9 @@ export class Product {
@Column({ default: true })
discountable: boolean
@Column({ nullable: true })
external_id: string
@CreateDateColumn({ type: resolveDbType("timestamptz") })
created_at: Date
+38
View File
@@ -393,6 +393,44 @@ class OrderService extends BaseService {
return order
}
/**
* Gets an order by id.
* @param {string} externalId - id of order to retrieve
* @param {object} config - query config to get order by
* @return {Promise<Order>} the order document
*/
async retrieveByExternalId(externalId, config = {}) {
const orderRepo = this.manager_.getCustomRepository(this.orderRepository_)
const { select, relations, totalsToSelect } =
this.transformQueryForTotals_(config)
const query = {
where: { external_id: externalId },
}
if (relations && relations.length > 0) {
query.relations = relations
}
if (select && select.length > 0) {
query.select = select
}
const rels = query.relations
delete query.relations
const raw = await orderRepo.findOneWithRelations(rels, query)
if (!raw) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Order with external id ${externalId} was not found`
)
}
const order = this.decorateTotals_(raw, totalsToSelect)
return order
}
/**
* Checks the existence of an order by cart id.
* @param {string} cartId - cart id to find order
@@ -70,6 +70,30 @@ class ProductCollectionService extends BaseService {
return collection
}
/**
* Retrieves a product collection by id.
* @param {string} collectionHandle - the handle of the collection to retrieve.
* @param {object} config - query config for request
* @return {Promise<ProductCollection>} the collection.
*/
async retrieveByHandle(collectionHandle, config = {}) {
const collectionRepo = this.manager_.getCustomRepository(
this.productCollectionRepository_
)
const query = this.buildQuery_({ handle: collectionHandle }, config)
const collection = await collectionRepo.findOne(query)
if (!collection) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product collection with handle: ${collectionHandle} was not found`
)
}
return collection
}
/**
* Creates a product collection
* @param {object} collection - the collection to create
@@ -614,6 +614,7 @@ class ProductVariantService extends BaseService {
.emit(ProductVariantService.Events.DELETED, {
id: variant.id,
product_id: variant.product_id,
metadata: variant.metadata,
})
return Promise.resolve()
+72
View File
@@ -201,6 +201,78 @@ class ProductService extends BaseService {
return product
}
/**
* Gets a product by handle.
* Throws in case of DB Error and if product was not found.
* @param {string} productHandle - handle of the product to get.
* @param {object} config - details about what to get from the product
* @return {Promise<Product>} the result of the find one operation.
*/
async retrieveByHandle(productHandle, config = {}) {
const productRepo = this.manager_.getCustomRepository(
this.productRepository_
)
const query = { where: { handle: productHandle } }
if (config.relations && config.relations.length > 0) {
query.relations = config.relations
}
if (config.select && config.select.length > 0) {
query.select = config.select
}
const rels = query.relations
delete query.relations
const product = await productRepo.findOneWithRelations(rels, query)
if (!product) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with handle: ${productHandle} was not found`
)
}
return product
}
/**
* Gets a product by external id.
* Throws in case of DB Error and if product was not found.
* @param {string} externalId - handle of the product to get.
* @param {object} config - details about what to get from the product
* @return {Promise<Product>} the result of the find one operation.
*/
async retrieveByExternalId(externalId, config = {}) {
const productRepo = this.manager_.getCustomRepository(
this.productRepository_
)
const query = { where: { external_id: externalId } }
if (config.relations && config.relations.length > 0) {
query.relations = config.relations
}
if (config.select && config.select.length > 0) {
query.select = config.select
}
const rels = query.relations
delete query.relations
const product = await productRepo.findOneWithRelations(rels, query)
if (!product) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`Product with exteral_id: ${externalId} was not found`
)
}
return product
}
/**
* Gets all variants belonging to a product.
* @param {string} productId - the id of the product to get variants from.
+28
View File
@@ -336,6 +336,34 @@ class RegionService extends BaseService {
return country
}
async retrieveByCountryCode(code, config = {}) {
const countryRepository = this.manager_.getCustomRepository(
this.countryRepository_
)
const country = await countryRepository.findOne({
where: {
iso_2: code.toLowerCase(),
},
})
if (!country) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Country with code ${code} not found`
)
}
if (!country.region_id) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`Country does not belong to a region`
)
}
return await this.retrieve(country.region_id, config)
}
/**
* Retrieves a region by its id.
* @param {string} regionId - the id of the region to retrieve