Skip to content

feat: shop / marketplace #301

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
754dcdf
feat: add partial api for shop
johngerome Nov 1, 2022
05495b9
feat: add /products/:id endpoint
johngerome Nov 1, 2022
2d40c36
feat: archive product
johngerome Nov 1, 2022
52e7c34
feat: restore product
johngerome Nov 1, 2022
60ca3a0
feat: cache products
johngerome Nov 1, 2022
89aee86
Merge branch 'main' into feat-shop
johngerome Nov 2, 2022
dadf53f
feat: cache get product details
johngerome Nov 2, 2022
5f6c89d
Merge branch 'main' into feat-shop
johngerome Nov 2, 2022
936bb95
Merge branch 'main' into feat-shop
johngerome Nov 2, 2022
1982759
feat: clear cache on create product
johngerome Nov 2, 2022
e02885c
Merge branch 'main' into feat-shop
johngerome Nov 2, 2022
6f1aa14
feat: add clear cache endpoint
johngerome Nov 2, 2022
30406c5
Merge branch 'main' into feat-shop
johngerome Nov 2, 2022
f19fbf1
feat: add currency settings and price
johngerome Nov 3, 2022
fc303df
Merge branch 'feat-shop' of github.com:RxCP/rxcp into feat-shop
johngerome Nov 3, 2022
1e4fe51
feat: setup order
johngerome Nov 3, 2022
8d13396
Merge branch 'main' into feat-shop
johngerome Nov 4, 2022
6732c6c
Merge branch 'main' into feat-shop
johngerome Nov 4, 2022
495db7b
Merge branch 'main' into feat-shop
johngerome Nov 6, 2022
042204f
Merge branch 'main' into feat-shop
johngerome Nov 6, 2022
175a1fa
Merge branch 'main' into feat-shop
johngerome Nov 6, 2022
1733bd0
Merge branch 'main' into feat-shop
johngerome Nov 7, 2022
7b9d504
Merge branch 'main' into feat-shop
johngerome Nov 7, 2022
60e9762
Merge branch 'main' into feat-shop
johngerome Nov 13, 2022
a52c499
Merge branch 'main' into feat-shop
johngerome Nov 13, 2022
5fe8cad
Merge branch 'main' into feat-shop
johngerome Nov 13, 2022
8c3bd57
Merge branch 'main' into feat-shop
johngerome Nov 13, 2022
e2b7711
Merge branch 'main' into feat-shop
johngerome Nov 14, 2022
aebd57e
Merge branch 'main' into feat-shop
johngerome Nov 15, 2022
824c482
Merge branch 'main' into feat-shop
johngerome Nov 16, 2022
27892cf
Merge branch 'main' into feat-shop
johngerome Nov 16, 2022
8c7c15e
Merge branch 'main' into feat-shop
johngerome Nov 17, 2022
d5f082d
Merge branch 'main' into feat-shop
johngerome Nov 17, 2022
24f5e15
feat(api): products should belongs to user
johngerome Nov 17, 2022
2f0c00b
Merge branch 'main' into feat-shop
johngerome Nov 18, 2022
1338a27
Merge branch 'main' into feat-shop
johngerome Nov 18, 2022
2559bcb
Merge branch 'main' into feat-shop
johngerome Nov 18, 2022
243456b
Merge branch 'main' into feat-shop
johngerome Nov 18, 2022
5f1c386
feat(api): update orders data and add few sample controllers (admin /…
johngerome Nov 18, 2022
5ab4a70
Merge branch 'main' into feat-shop
johngerome Dec 24, 2022
7c1d9ef
chore: remove .prettierrc.js infavor of .prettierrc
johngerome Dec 24, 2022
faf19a6
Merge branch 'main' into feat-shop
johngerome Jan 1, 2023
6a4e493
Merge branch 'main' into feat-shop
johngerome Feb 19, 2023
5e0f487
Merge branch 'main' into feat-shop
johngerome Feb 20, 2023
c5d7d54
Merge branch 'main' into feat-shop
johngerome Feb 24, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/api/app/Controllers/Http/Admin/Shop/OrdersController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Order from 'App/Models/Shop/Order'

export default class OrdersController {
/**
* Order list
*/
public async index({ request, bouncer }: HttpContextContract) {
await bouncer.with('RolePolicy').authorize('permission', 'api::orders.index')

const page = request.input('page', 1)
const limit = request.input('limit', 10)
return Order.query().paginate(page, limit)
}
}
172 changes: 172 additions & 0 deletions apps/api/app/Controllers/Http/Admin/Shop/ProductsController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import { schema } from '@ioc:Adonis/Core/Validator'
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Product from 'App/Models/Shop/Product'
import {
descriptionRules,
priceRules,
slugRules,
statusRules,
titleRules,
} from 'App/Validations/product'
import cacheData, { purgeCache } from 'App/Services/cacheData'

export default class ProductsController {
private cachePrefix = 'products'

/**
* Product list
*/
public async index({ request, response, bouncer }: HttpContextContract) {
await bouncer.with('RolePolicy').authorize('permission', 'api::shop::products.index')
const page = request.input('page', 1)
const limit = request.input('limit', 10)
const requestQs = request.qs()
const qs = JSON.stringify(requestQs)
const cacheKey = qs !== '{}' ? `${this.cachePrefix}:${qs}` : this.cachePrefix

return await cacheData(cacheKey)(response)(async () => {
return await Product.query().preload('user').filter(requestQs).paginate(page, limit)
})
}

/**
* Show product details
*/
public async show({ params, response, bouncer }: HttpContextContract) {
await bouncer.with('RolePolicy').authorize('permission', 'api::shop::products.show')

const data = await cacheData(`${this.cachePrefix}:${params?.id}`)(response)(async () => {
return await Product.find(params?.id)
})

return {
data,
}
}

/**
* Create Product
*/
public async create({ auth, request, response, bouncer }: HttpContextContract) {
await bouncer.with('RolePolicy').authorize('permission', 'api::shop::products.create')
const payload = request.only(['title', 'description', 'slug', 'status', 'price'])

const userId = auth.use('api').user?.id

// Validation
const createProductSchema = schema.create({
title: titleRules,
description: descriptionRules,
slug: slugRules(),
status: statusRules,
price: priceRules,
})

await request.validate({ schema: createProductSchema })

try {
const product = await Product.create({
title: payload.title,
description: payload.description,
slug: payload.slug,
status: payload.status,
price: payload.price,
user_id: userId,
})

await this.purgeCache()

return product
} catch (e) {
return response.badRequest({
errors: [
{
message: e.toString(),
},
],
})
}
}

/**
* Archive product
*/
public async archive({ params, response, bouncer }: HttpContextContract) {
await bouncer.with('RolePolicy').authorize('permission', 'api::shop::products.archive')

try {
const product = await Product.findOrFail(params?.id)
await product.delete()
return response.noContent()
} catch (e) {
return response.badRequest({
errors: [
{
message: e.toString(),
},
],
})
}
}

/**
* Archived products
*/
public async archived({ request, bouncer }: HttpContextContract) {
await bouncer.with('RolePolicy').authorize('permission', 'api::shop::products.archived')

const page = request.input('page', 1)
const limit = request.input('limit', 10)

return await Product.query().onlyTrashed().filter(request.qs()).paginate(page, limit)
}

/**
* Restore product from archived
*/
public async restore({ request, response, bouncer }: HttpContextContract) {
await bouncer.with('RolePolicy').authorize('permission', 'api::shop::products.restore')
const payload = request.only(['product_id'])

try {
const product = await Product.withTrashed().where('id', payload.product_id).firstOrFail()
await product.restore()
return product
} catch (e) {
return response.badRequest({
errors: [
{
message: e.toString(),
},
],
})
}
}

/**
* Clear one product cache
*/
public async clearOneCache({ response, params, bouncer }: HttpContextContract) {
await bouncer.with('RolePolicy').authorize('permission', 'api::shop::products.clearCache')
await this.purgeCache(params?.id)

return response.noContent()
}

/**
* Clear products cache
*/
public async clearAllCache({ bouncer, response }: HttpContextContract) {
await bouncer.with('RolePolicy').authorize('permission', 'api::shop::products.clearCache')
await this.purgeCache()

return response.noContent()
}

/**
* Clear cache
*/
private async purgeCache(id?: string) {
purgeCache(id ? `${this.cachePrefix}:${id}` : `${this.cachePrefix}*`)
}
}
15 changes: 15 additions & 0 deletions apps/api/app/Controllers/Http/Shop/OrdersController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Order from 'App/Models/Shop/Order'

export default class OrdersController {
/**
* Guild list
*/
public async index({ auth, request }: HttpContextContract) {
const authId = auth.use('api').user?.id || ''

const page = request.input('page', 1)
const limit = 10
return Order.query().where('customer_id', authId).paginate(page, limit)
}
}
7 changes: 7 additions & 0 deletions apps/api/app/Models/Filters/ProductFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { ModelQueryBuilderContract } from '@ioc:Adonis/Lucid/Orm'
import Product from '../Shop/Product'
import BasePowerFilter from './BasePowerFilter'

export default class ProductFilter extends BasePowerFilter {
public $query: ModelQueryBuilderContract<typeof Product, Product>
}
37 changes: 37 additions & 0 deletions apps/api/app/Models/Shop/Order.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { DateTime } from 'luxon'
import { BaseModel, column } from '@ioc:Adonis/Lucid/Orm'

export default class Order extends BaseModel {
@column({ isPrimary: true })
public id: number

@column()
public customer: object

@column()
public cart: object

@column()
public payments: object

@column()
public payment_status: string

@column()
public total: number

@column()
public subtotal: number

@column()
public paid_total: number

@column()
public discount_total: number

@column.dateTime({ autoCreate: true })
public createdAt: DateTime

@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
}
47 changes: 47 additions & 0 deletions apps/api/app/Models/Shop/Product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { DateTime } from 'luxon'
import { BaseModel, belongsTo, BelongsTo, column } from '@ioc:Adonis/Lucid/Orm'
import { Filterable } from '@ioc:Adonis/Addons/LucidFilter'
import { SoftDeletes } from '@ioc:Adonis/Addons/LucidSoftDeletes'
import { compose } from '@ioc:Adonis/Core/Helpers'
import ProductFilter from '../Filters/ProductFilter'
import User from '../User'

export default class Product extends compose(BaseModel, Filterable, SoftDeletes) {
public static $filter = () => ProductFilter

@column({ isPrimary: true })
public id: number

@column()
public title: string

@column()
public description: string

@column()
public slug: string

@column()
public price: number

@column()
public status: 'draft' | 'published'

@column({ serializeAs: null })
public user_id: number

@belongsTo(() => User, {
foreignKey: 'user_id',
localKey: 'id'
})
public user: BelongsTo<typeof User>

@column.dateTime()
public deletedAt: DateTime | null

@column.dateTime({ autoCreate: true })
public createdAt: DateTime

@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime
}
15 changes: 15 additions & 0 deletions apps/api/app/Validations/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { schema, rules } from '@ioc:Adonis/Core/Validator'

export const titleRules = schema.string({ trim: true }, [rules.maxLength(20)])
export const descriptionRules = schema.string({ trim: true }, [rules.maxLength(1000)])
export const slugRules = (id?: number) => {
let fieldRules = [rules.maxLength(50)]
if (id) {
fieldRules.push(rules.unique({ table: 'products', column: 'slug', whereNot: { id } }))
} else {
fieldRules.push(rules.unique({ table: 'products', column: 'slug' }))
}
return schema.string({ trim: true }, fieldRules)
}
export const statusRules = schema.string({ trim: true }, [rules.maxLength(10)])
export const priceRules = schema.number([rules.range(0, 999999999)])
29 changes: 29 additions & 0 deletions apps/api/database/migrations/shop/1667297197047_product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class extends BaseSchema {
protected tableName = 'products'

public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')

table.string('title', 255).notNullable()
table.text('description', 'longtext')
table.string('slug', 50).notNullable().unique()
table.string('status', 10).notNullable()
table.integer('price').notNullable()
table.integer('user_id').unsigned().references('users.id').onDelete('cascade')
table.timestamp('deleted_at', { useTz: true }).nullable()

/**
* Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
*/
table.timestamp('created_at', { useTz: true })
table.timestamp('updated_at', { useTz: true })
})
}

public async down() {
this.schema.dropTable(this.tableName)
}
}
38 changes: 38 additions & 0 deletions apps/api/database/migrations/shop/1667480498405_orders.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import BaseSchema from '@ioc:Adonis/Lucid/Schema'

export default class extends BaseSchema {
protected tableName = 'orders'

public async up() {
this.schema.createTable(this.tableName, (table) => {
table.increments('id')

table.string('email', 100).notNullable()
table.string('currency_code', 10).notNullable()
table.string('payment_status').notNullable()
table.integer('total').notNullable()
table.integer('subtotal').notNullable()
table.integer('paid_total').notNullable()
table.integer('coupon').nullable()
table.integer('discount_total').nullable()
table.integer('tax_rate').nullable()
table.integer('tax_total').nullable()
table.integer('refunded_total').nullable()
table.integer('refundable_amount').nullable()
table.string('status').notNullable()
table.integer('cart_id').notNullable()
table.integer('customer_id').unsigned().references('users.id').onDelete('cascade')

/**
* Uses timestamptz for PostgreSQL and DATETIME2 for MSSQL
*/
table.timestamp('created_at', { useTz: true })
table.timestamp('updated_at', { useTz: true })
table.timestamp('canceled_at', { useTz: true })
})
}

public async down() {
this.schema.dropTable(this.tableName)
}
}
5 changes: 5 additions & 0 deletions apps/api/database/seeders/MainSeeder/Permission.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export default class extends BaseSeeder {
{ action: 'api::accounts.delete' },
{ action: 'api::characters.index' },
{ action: 'api::guilds.index' },
{ action: 'api::shop::product.index' },
{ action: 'api::shop::product.show' },
{ action: 'api::shop::product.create' },
{ action: 'api::shop::product.archive' },
{ action: 'api::shop::product.archived' },
])
}
}
Loading