user restuarant table
This commit is contained in:
@@ -0,0 +1,20 @@
|
|||||||
|
import { Migration } from '@mikro-orm/migrations';
|
||||||
|
|
||||||
|
export class Migration20260628130000_AddUserShops extends Migration {
|
||||||
|
|
||||||
|
override async up(): Promise<void> {
|
||||||
|
this.addSql(`create table "user_shops" ("id" char(26) not null, "created_at" timestamptz not null default now(), "updated_at" timestamptz not null default now(), "deleted_at" timestamptz null, "order_count" int not null default 0, "total_order_amount" int not null default 0, "user_id" char(26) not null, "shop_id" char(26) not null, constraint "user_shops_pkey" primary key ("id"));`);
|
||||||
|
this.addSql(`create index "user_shops_created_at_index" on "user_shops" ("created_at");`);
|
||||||
|
this.addSql(`create index "user_shops_deleted_at_index" on "user_shops" ("deleted_at");`);
|
||||||
|
this.addSql(`create index "user_shops_user_id_shop_id_index" on "user_shops" ("user_id", "shop_id");`);
|
||||||
|
this.addSql(`alter table "user_shops" add constraint "user_shops_user_id_shop_id_unique" unique ("user_id", "shop_id");`);
|
||||||
|
|
||||||
|
this.addSql(`alter table "user_shops" add constraint "user_shops_user_id_foreign" foreign key ("user_id") references "users" ("id") on update cascade on delete cascade;`);
|
||||||
|
this.addSql(`alter table "user_shops" add constraint "user_shops_shop_id_foreign" foreign key ("shop_id") references "shops" ("id") on update cascade on delete cascade;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async down(): Promise<void> {
|
||||||
|
this.addSql(`drop table if exists "user_shops" cascade;`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -75,6 +75,8 @@ export class AuthService {
|
|||||||
throw new BadRequestException(ShopMessage.NOT_FOUND);
|
throw new BadRequestException(ShopMessage.NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.userService.ensureUserShopMembership(user.id, shop.id);
|
||||||
|
|
||||||
const tokens = await this.tokensService.generateTokens(user.id, shop.id, false, slug);
|
const tokens = await this.tokensService.generateTokens(user.id, shop.id, false, slug);
|
||||||
|
|
||||||
const userResponse = UserLoginTransformer.transform(user, shop);
|
const userResponse = UserLoginTransformer.transform(user, shop);
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Cron } from '@nestjs/schedule';
|
||||||
|
import { EntityManager } from '@mikro-orm/postgresql';
|
||||||
|
import { UserShop } from '../entities/user-shop.entity';
|
||||||
|
import { User } from '../entities/user.entity';
|
||||||
|
import { Shop } from '../../shops/entities/shop.entity';
|
||||||
|
import { OrderStatus } from '../../orders/interface/order.interface';
|
||||||
|
|
||||||
|
interface UserShopAggregateRow {
|
||||||
|
user_id: string;
|
||||||
|
shop_id: string;
|
||||||
|
order_count: string | number;
|
||||||
|
total_order_amount: string | number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const COUNTED_ORDER_STATUSES = [
|
||||||
|
OrderStatus.PAID,
|
||||||
|
OrderStatus.PREPARING,
|
||||||
|
OrderStatus.DELIVERED_TO_RECEPIENT,
|
||||||
|
OrderStatus.SHIPPED,
|
||||||
|
OrderStatus.COMPLETED,
|
||||||
|
];
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserShopCrone {
|
||||||
|
private readonly logger = new Logger(UserShopCrone.name);
|
||||||
|
|
||||||
|
constructor(private readonly em: EntityManager) { }
|
||||||
|
|
||||||
|
@Cron('0 3 * * *', {
|
||||||
|
name: 'syncUserShopOrderStats',
|
||||||
|
timeZone: 'UTC',
|
||||||
|
})
|
||||||
|
async syncUserShopOrderStats() {
|
||||||
|
try {
|
||||||
|
this.logger.log('Starting daily user-shop order stats sync');
|
||||||
|
|
||||||
|
const statusPlaceholders = COUNTED_ORDER_STATUSES.map(() => '?').join(', ');
|
||||||
|
|
||||||
|
const aggregates = await this.em.execute<UserShopAggregateRow[]>(
|
||||||
|
`
|
||||||
|
SELECT
|
||||||
|
o.user_id,
|
||||||
|
o.shop_id,
|
||||||
|
COUNT(*)::int AS order_count,
|
||||||
|
COALESCE(SUM(o.total), 0)::int AS total_order_amount
|
||||||
|
FROM orders o
|
||||||
|
WHERE o.deleted_at IS NULL
|
||||||
|
AND o.status IN (${statusPlaceholders})
|
||||||
|
GROUP BY o.user_id, o.shop_id
|
||||||
|
`,
|
||||||
|
COUNTED_ORDER_STATUSES,
|
||||||
|
);
|
||||||
|
|
||||||
|
await this.em.nativeUpdate(
|
||||||
|
UserShop,
|
||||||
|
{ deletedAt: null },
|
||||||
|
{ orderCount: 0, totalOrderAmount: 0 },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!aggregates.length) {
|
||||||
|
this.logger.log('No qualifying orders found; user-shop stats reset to zero');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const row of aggregates) {
|
||||||
|
let userShop = await this.em.findOne(UserShop, {
|
||||||
|
user: row.user_id,
|
||||||
|
shop: row.shop_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!userShop) {
|
||||||
|
userShop = this.em.create(UserShop, {
|
||||||
|
user: this.em.getReference(User, row.user_id),
|
||||||
|
shop: this.em.getReference(Shop, row.shop_id),
|
||||||
|
orderCount: 0,
|
||||||
|
totalOrderAmount: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
userShop.orderCount = Number(row.order_count);
|
||||||
|
userShop.totalOrderAmount = Number(row.total_order_amount);
|
||||||
|
this.em.persist(userShop);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.em.flush();
|
||||||
|
|
||||||
|
this.logger.log(`Synced order stats for ${aggregates.length} user-shop pairs`);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.error(`UserShopCrone failed: ${(err as Error)?.message}`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { Entity, Index, ManyToOne, Property, Unique } from '@mikro-orm/core';
|
||||||
|
import { BaseEntity } from '../../../common/entities/base.entity';
|
||||||
|
import { User } from './user.entity';
|
||||||
|
import { Shop } from '../../shops/entities/shop.entity';
|
||||||
|
|
||||||
|
@Entity({ tableName: 'user_shops' })
|
||||||
|
@Unique({ properties: ['user', 'shop'] })
|
||||||
|
@Index({ properties: ['user', 'shop'] })
|
||||||
|
export class UserShop extends BaseEntity {
|
||||||
|
@ManyToOne(() => User)
|
||||||
|
user!: User;
|
||||||
|
|
||||||
|
@ManyToOne(() => Shop)
|
||||||
|
shop!: Shop;
|
||||||
|
|
||||||
|
@Property({ type: 'integer', default: 0 })
|
||||||
|
orderCount: number = 0;
|
||||||
|
|
||||||
|
@Property({ type: 'integer', default: 0 })
|
||||||
|
totalOrderAmount: number = 0;
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@ import { WalletService } from './wallet.service';
|
|||||||
import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet';
|
import { WalletTransactionReason, WalletTransactionType } from '../interface/wallet';
|
||||||
import { WalletTransaction } from '../entities/wallet-transaction.entity';
|
import { WalletTransaction } from '../entities/wallet-transaction.entity';
|
||||||
import { ShopMessage, UserMessage } from 'src/common/enums/message.enum';
|
import { ShopMessage, UserMessage } from 'src/common/enums/message.enum';
|
||||||
|
import { UserShop } from '../entities/user-shop.entity';
|
||||||
|
import { UserShopRepository } from '../repositories/user-shop.repository';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserService {
|
export class UserService {
|
||||||
@@ -27,6 +29,7 @@ export class UserService {
|
|||||||
private readonly restaurantRepository: ShopRepository,
|
private readonly restaurantRepository: ShopRepository,
|
||||||
private readonly walletTransactionRepository: WalletTransactionRepository,
|
private readonly walletTransactionRepository: WalletTransactionRepository,
|
||||||
private readonly walletService: WalletService,
|
private readonly walletService: WalletService,
|
||||||
|
private readonly userShopRepository: UserShopRepository,
|
||||||
private readonly em: EntityManager,
|
private readonly em: EntityManager,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@@ -55,6 +58,25 @@ export class UserService {
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async ensureUserShopMembership(userId: string, shopId: string): Promise<UserShop> {
|
||||||
|
const existing = await this.userShopRepository.findOne({ user: userId, shop: shopId });
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userShop = this.userShopRepository.create({
|
||||||
|
user: this.em.getReference(User, userId),
|
||||||
|
shop: this.em.getReference(Shop, shopId),
|
||||||
|
orderCount: 0,
|
||||||
|
totalOrderAmount: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.em.persistAndFlush(userShop);
|
||||||
|
|
||||||
|
return userShop;
|
||||||
|
}
|
||||||
|
|
||||||
// async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
|
// async updateUser(userId: string, dto: UpdateUserDto): Promise<User> {
|
||||||
// const user = await this.em.findOneOrFail(User, userId, {}).catch(() => {
|
// const user = await this.em.findOneOrFail(User, userId, {}).catch(() => {
|
||||||
// throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
// throw new NotFoundException(UserMessage.USER_NOT_FOUND);
|
||||||
@@ -129,46 +151,37 @@ export class UserService {
|
|||||||
async findAll(shopId: string, dto: FindUsersDto): Promise<PaginatedResult<User>> {
|
async findAll(shopId: string, dto: FindUsersDto): Promise<PaginatedResult<User>> {
|
||||||
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;
|
const { page = 1, limit = 10, search, orderBy = 'createdAt', order = 'desc' } = dto;
|
||||||
|
|
||||||
// 1. Calculate pagination
|
|
||||||
const offset = (page - 1) * limit;
|
const offset = (page - 1) * limit;
|
||||||
|
|
||||||
// 2. Build the 'where' filter query
|
const where: FilterQuery<UserShop> = {
|
||||||
const where: FilterQuery<User> = {
|
shop: { id: shopId },
|
||||||
orders: {
|
|
||||||
shop: {
|
|
||||||
id: shopId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 4. Add 'search' logic (case-insensitive)
|
|
||||||
if (search) {
|
if (search) {
|
||||||
const searchPattern = `%${search}%`;
|
const searchPattern = `%${search}%`;
|
||||||
const normalizedSearch = normalizePhone(search);
|
const normalizedSearch = normalizePhone(search);
|
||||||
const normalizedSearchPattern = `%${normalizedSearch}%`;
|
const normalizedSearchPattern = `%${normalizedSearch}%`;
|
||||||
// $ilike is case-insensitive (PostgreSQL). Use $like for MySQL (often CI by default)
|
where.user = {
|
||||||
// Search with both original and normalized phone patterns to handle various input formats
|
$or: [
|
||||||
where.$or = [
|
{ firstName: { $ilike: searchPattern } },
|
||||||
{ firstName: { $ilike: searchPattern } },
|
{ lastName: { $ilike: searchPattern } },
|
||||||
{ lastName: { $ilike: searchPattern } },
|
{ phone: { $ilike: searchPattern } },
|
||||||
{ phone: { $ilike: searchPattern } },
|
...(normalizedSearch !== search ? [{ phone: { $ilike: normalizedSearchPattern } }] : []),
|
||||||
...(normalizedSearch !== search ? [{ phone: { $ilike: normalizedSearchPattern } }] : []),
|
],
|
||||||
];
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Execute the query using findAndCount
|
const [userShops, total] = await this.userShopRepository.findAndCount(where, {
|
||||||
const [users, total] = await this.userRepository.findAndCount(where, {
|
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
orderBy: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' },
|
orderBy: { user: { [orderBy]: order.toLowerCase() as 'asc' | 'desc' } },
|
||||||
|
populate: ['user'],
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6. Calculate total pages
|
|
||||||
const totalPages = Math.ceil(total / limit);
|
const totalPages = Math.ceil(total / limit);
|
||||||
|
|
||||||
// 7. Return the paginated result
|
|
||||||
return {
|
return {
|
||||||
data: users,
|
data: userShops.map(userShop => userShop.user),
|
||||||
meta: {
|
meta: {
|
||||||
total,
|
total,
|
||||||
page,
|
page,
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { EntityManager, EntityRepository } from '@mikro-orm/postgresql';
|
||||||
|
import { UserShop } from '../entities/user-shop.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserShopRepository extends EntityRepository<UserShop> {
|
||||||
|
constructor(readonly em: EntityManager) {
|
||||||
|
super(em, UserShop);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,17 +13,20 @@ import { PointTransaction } from './entities/point-transaction.entity';
|
|||||||
import { PointTransactionRepository } from './repositories/point-transaction.repository';
|
import { PointTransactionRepository } from './repositories/point-transaction.repository';
|
||||||
import { ShopsModule } from '../shops/shops.module';
|
import { ShopsModule } from '../shops/shops.module';
|
||||||
import { AddressRepository } from './repositories/address.repository';
|
import { AddressRepository } from './repositories/address.repository';
|
||||||
|
import { UserShop } from './entities/user-shop.entity';
|
||||||
|
import { UserShopRepository } from './repositories/user-shop.repository';
|
||||||
|
import { UserShopCrone } from './crone/user-shop.crone';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
providers: [UserService, WalletService, UserRepository,
|
providers: [UserService, WalletService, UserRepository,
|
||||||
WalletTransactionRepository, PointTransactionRepository, AddressRepository],
|
WalletTransactionRepository, PointTransactionRepository, AddressRepository, UserShopRepository, UserShopCrone],
|
||||||
controllers: [UsersController],
|
controllers: [UsersController],
|
||||||
imports: [
|
imports: [
|
||||||
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction]),
|
MikroOrmModule.forFeature([User, UserAddress, WalletTransaction, PointTransaction, UserShop]),
|
||||||
JwtModule,
|
JwtModule,
|
||||||
ShopsModule
|
ShopsModule
|
||||||
],
|
],
|
||||||
exports: [UserService, WalletService, UserRepository,
|
exports: [UserService, WalletService, UserRepository,
|
||||||
WalletTransactionRepository, PointTransactionRepository, AddressRepository],
|
WalletTransactionRepository, PointTransactionRepository, AddressRepository, UserShopRepository],
|
||||||
})
|
})
|
||||||
export class UserModule { }
|
export class UserModule { }
|
||||||
|
|||||||
Reference in New Issue
Block a user