Skip to content

Commit

Permalink
initial
Browse files Browse the repository at this point in the history
  • Loading branch information
prasmalla committed May 14, 2023
0 parents commit 18d9332
Show file tree
Hide file tree
Showing 19 changed files with 12,637 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Editor configuration, see http://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true

[*.json]
indent_size = 2

[*.yml]
indent_size = 2

[*.md]
max_line_length = off
trim_trailing_whitespace = false
1 change: 1 addition & 0 deletions .github/funding.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: prasmalla
32 changes: 32 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
*.launch
*.log
*.sqlite
*.sublime-workspace
.DS_Store
.c9/
.cache
.classpath
.env
.env.*
.history
.idea
.project
.sass-cache
.settings/
.temp
.vscode
Thumbs.db
__data__
bazel-out
chrome-profiler-events*.json
connect.lock
coverage
dist
generated/
lib/
libpeerconnection.log
node_modules
out-tsc
speed-measure-plugin*.json
tmp
typings
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
generated-types.ts
lazy-extensions.module.ts
shared-extensions.module.ts
generated-graphql-shop-errors.ts
generated-graphql-admin-errors.ts
6 changes: 6 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 110,
"arrowParens": "avoid"
}
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
## Printful Vendure Plugin

![Vendure version](https://img.shields.io/npm/dependency-version/@callit-today/vendure-plugin-printful/dev/@vendure/core)

Import Printful products and start selling!


# Getting started

`yarn add @callit-today/vendure-plugin-printful`

\
 
Add your `PRINTFUL_AUTH_TOKEN` to `.env` and run import GraphQL query in admin-api

```graphql
{
importPrintfulProducts
}
```
\
 

## How it works

The plugin uses `FastImporterSerivce` to import products from Printful. Remember to rebuild search index after import completes. When an order is completed, a draft order is created in Printful.

 

## Next steps

Confirm the order in Printful for fulfillment.

 

## Todo

Add config option for auto confirming orders on Printful.

\
 

## License

MIT
37 changes: 37 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@callit-today/vendure-plugin-printful",
"version": "0.0.2",
"description": "Vendure Plugin for Printful",
"author": "CALLiT.today <[email protected]>",
"repository": "https://github.com/calliT-today/vendure-plugin-printful",
"license": "MIT",
"private": false,
"engines": {
"node": ">=16.0.0"
},
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist",
"README.md"
],
"scripts": {
"start": "yarn ts-node test/dev-server.ts",
"build": "rimraf dist && tsc",
"test": "jest --preset=\"ts-jest\""
},
"devDependencies": {
"@types/jest": "29.4.0",
"@vendure/admin-ui-plugin": "1.9.6",
"@vendure/asset-server-plugin": "1.9.6",
"@vendure/core": "1.9.6",
"@vendure/testing": "1.9.6",
"@vendure/ui-devkit": "1.9.6",
"jest": "29.4.3",
"printful-client": "^0.0.3",
"rimraf": "^4.1.2",
"ts-jest": "29.0.5",
"ts-node": "10.9.1",
"typescript": "4.9.5"
}
}
7 changes: 7 additions & 0 deletions src/api/api-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { gql } from 'graphql-tag';

export const adminSchema = gql`
extend type Query {
importPrintfulProducts: String
}
`;
14 changes: 14 additions & 0 deletions src/api/printful.resolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Resolver, Query } from '@nestjs/graphql';
import { Permission, Allow, RequestContext, Ctx } from '@vendure/core';
import { PrintfulService } from '../service/printful.service';

@Resolver()
export class PrintfulResolver {
constructor(private printfulService: PrintfulService) {}

@Query()
@Allow(Permission.Public)
async importPrintfulProducts(@Ctx() ctx: RequestContext): Promise<void> {
this.printfulService.importPrintfulProducts(ctx);
}
}
2 changes: 2 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const loggerCtx = 'PrintfulPlugin';
export const PLUGIN_INIT_OPTIONS = Symbol('PRINTFUL_PLUGIN_INIT_OPTIONS');
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './printful.plugin';
78 changes: 78 additions & 0 deletions src/printful.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { Order, PluginCommonModule, TransactionalConnection, VendurePlugin } from '@vendure/core';
import { adminSchema } from './api/api-extensions';
import { PrintfulResolver } from './api/printful.resolver';
import { PLUGIN_INIT_OPTIONS } from './constants';
import { PrintfulClient } from 'printful-client';
import { PrintfulService } from './service/printful.service';
import { EntitySubscriberInterface, EventSubscriber, UpdateEvent } from 'typeorm';
import { Injectable } from '@nestjs/common';

export interface PrintfulOptions {
enabled: boolean;
}

@Injectable()
@EventSubscriber()
export class OrderSubscriber implements EntitySubscriberInterface<Order> {
constructor(private connection: TransactionalConnection) {
this.connection.rawConnection.subscribers.push(this);
}

listenTo() {
return Order;
}

async afterUpdate(event: UpdateEvent<Order>) {
if (event.entity?.state !== 'PaymentAuthorized') return;
try {
const printfulClient = new PrintfulClient(process.env.PRINTFUL_AUTH_TOKEN as string);
const orderItems = [];
for (const line of event.entity?.lines) {
const printfulOrderItem = {
sync_variant_id: line.productVariant.sku,
quantity: line.quantity,
retail_price: `${line.unitPrice / 100}`,
};
orderItems.push(printfulOrderItem);
}
const printfulOrder = {
recipient: {
name: event.entity?.shippingAddress?.fullName,
address1: event.entity?.shippingAddress?.streetLine1,
city: event.entity?.shippingAddress?.city,
state_code: event.entity?.shippingAddress?.province.match(/\b\w/g).join(''),
country_code: event.entity?.shippingAddress?.countryCode,
zip: event.entity?.shippingAddress?.postalCode,
},
items: orderItems,
};
await printfulClient.orders.create(printfulOrder);
} catch (e) {
console.log(e);
}
}
}

@VendurePlugin({
imports: [PluginCommonModule],
providers: [
{
provide: PLUGIN_INIT_OPTIONS,
useFactory: () => PrintfulPlugin.options,
},
PrintfulService,
OrderSubscriber,
],
adminApiExtensions: {
resolvers: [PrintfulResolver],
schema: adminSchema,
},
})
export class PrintfulPlugin {
static options: PrintfulOptions;

static init(options: PrintfulOptions) {
this.options = options;
return PrintfulPlugin;
}
}
108 changes: 108 additions & 0 deletions src/service/printful.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { Injectable } from '@nestjs/common';
import {
Asset,
AssetService,
ConfigService,
FastImporterService,
LanguageCode,
ProductService,
RequestContext,
} from '@vendure/core';
import {
CreateProductInput,
CreateProductVariantInput,
GlobalFlag,
ProductTranslationInput,
ProductVariantTranslationInput,
} from '../../generated/generated-types';
import { PrintfulClient } from 'printful-client';
import { normalizeString } from '@vendure/common/lib/normalize-string';

@Injectable()
export class PrintfulService {
private printfulClient: PrintfulClient;

constructor(
private fastImporterService: FastImporterService,
private configService: ConfigService,
private assetService: AssetService,
private productService: ProductService,
) {
this.printfulClient = new PrintfulClient(process.env.PRINTFUL_AUTH_TOKEN as string);
}

async getProductList(): Promise<any> {
try {
const response = await this.printfulClient.products.getAll();
return await response.json();
} catch (err) {
console.error(err);
}
}

async getProductById(id: string): Promise<any> {
try {
const response = await this.printfulClient.products.get(id);
return await response.json();
} catch (err) {
console.error(err);
}
}

async importPrintfulProducts(ctx: RequestContext) {
await this.fastImporterService.initialize(ctx.channel);
const { assetImportStrategy } = this.configService.importExportOptions;

const { result } = await this.getProductList();
for (const product of result) {
const existingProduct = await this.productService.findOneBySlug(
ctx,
normalizeString(product.name as string, '-'),
);
if (!existingProduct) {
const stream = await assetImportStrategy.getStreamFromPath(product.thumbnail_url);
const asset = (await this.assetService.createFromFileStream(stream, 'assets', ctx)) as Asset;

const productTranslation: ProductTranslationInput = {
languageCode: LanguageCode.en,
name: product.name,
slug: product.name,
description: 'Imported from Printful',
};
const createProductInput: CreateProductInput = {
featuredAssetId: asset.id as string,
customFields: { printfulProductId: product.id },
translations: [productTranslation],
};
const productId = await this.fastImporterService.createProduct(createProductInput);

const { result } = await this.getProductById(product.id);
for (const productVariant of result.sync_variants) {
const stream = await assetImportStrategy.getStreamFromPath(
productVariant.files[1].preview_url,
);
const asset = (await this.assetService.createFromFileStream(
stream,
'assets',
ctx,
)) as Asset;

const productVariantTranslation: ProductVariantTranslationInput = {
languageCode: LanguageCode.en,
name: productVariant.name,
};
const createProductVariantInput: CreateProductVariantInput = {
featuredAssetId: asset.id as string,
productId: productId as string,
translations: [productVariantTranslation],
sku: productVariant.id,
price: productVariant.retail_price * 100,
trackInventory: 'FALSE' as GlobalFlag,
taxCategoryId: '1',
};
await this.fastImporterService.createProductVariant(createProductVariantInput);
}
}
}
}
}
Loading

0 comments on commit 18d9332

Please sign in to comment.