mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 19:45:14 +00:00
phone syncs for shipping and stuff
This commit is contained in:
parent
02e66b488f
commit
b3b9a475de
10 changed files with 94 additions and 4 deletions
2
backend/drizzle/0009_add_phone_columns.sql
Normal file
2
backend/drizzle/0009_add_phone_columns.sql
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
ALTER TABLE users ADD COLUMN IF NOT EXISTS phone VARCHAR;
|
||||
ALTER TABLE shop_orders ADD COLUMN IF NOT EXISTS phone VARCHAR;
|
||||
|
|
@ -14,6 +14,7 @@ import slack from './routes/slack'
|
|||
import { startHackatimeSync } from './lib/hackatime-sync'
|
||||
import { startAirtableSync } from './lib/airtable-sync'
|
||||
import { startScrapsPayout } from './lib/scraps-payout'
|
||||
import { syncAllPhoneNumbers } from './lib/sync-phones'
|
||||
|
||||
const api = new Elysia()
|
||||
.use(authRoutes)
|
||||
|
|
@ -48,3 +49,6 @@ if (process.env.NODE_ENV !== 'development') {
|
|||
} else {
|
||||
console.log('[STARTUP] Skipping background syncs in development mode')
|
||||
}
|
||||
|
||||
// Sync phone numbers from Hack Club Auth on startup
|
||||
syncAllPhoneNumbers()
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ function buildJustification(project: {
|
|||
}, reviews: {
|
||||
action: string
|
||||
reviewerName: string | null
|
||||
internalJustification: string | null
|
||||
createdAt: Date
|
||||
}[], effectiveHours: number): string {
|
||||
const lines: string[] = []
|
||||
|
|
@ -55,6 +56,9 @@ function buildJustification(project: {
|
|||
const reviewerName = review.reviewerName || 'Unknown'
|
||||
const date = review.createdAt.toISOString().split('T')[0]
|
||||
lines.push(`- ${reviewerName} ${review.action} on ${date}`)
|
||||
if (review.internalJustification) {
|
||||
lines.push(` Justification: ${review.internalJustification}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -106,12 +110,13 @@ async function syncProjectsToAirtable(): Promise<void> {
|
|||
|
||||
// Fetch all reviews for shipped projects with reviewer usernames
|
||||
const projectIds = projects.map(p => p.id)
|
||||
let reviewsByProjectId = new Map<number, { action: string; reviewerName: string | null; createdAt: Date }[]>()
|
||||
let reviewsByProjectId = new Map<number, { action: string; reviewerName: string | null; internalJustification: string | null; createdAt: Date }[]>()
|
||||
if (projectIds.length > 0) {
|
||||
const allReviews = await db
|
||||
.select({
|
||||
projectId: reviewsTable.projectId,
|
||||
action: reviewsTable.action,
|
||||
internalJustification: reviewsTable.internalJustification,
|
||||
createdAt: reviewsTable.createdAt,
|
||||
reviewerUsername: usersTable.username
|
||||
})
|
||||
|
|
@ -124,6 +129,7 @@ async function syncProjectsToAirtable(): Promise<void> {
|
|||
existing.push({
|
||||
action: review.action,
|
||||
reviewerName: review.reviewerUsername,
|
||||
internalJustification: review.internalJustification,
|
||||
createdAt: review.createdAt
|
||||
})
|
||||
reviewsByProjectId.set(review.projectId, existing)
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ interface HackClubIdentity {
|
|||
verification_status?: string
|
||||
primary_email?: string
|
||||
slack_id?: string
|
||||
phone_number?: string
|
||||
given_name?: string
|
||||
family_name?: string
|
||||
birthdate?: string
|
||||
|
|
@ -160,6 +161,7 @@ export async function createOrUpdateUser(identity: HackClubIdentity, tokens: OID
|
|||
username,
|
||||
email: identity.primary_email || "",
|
||||
avatar: avatarUrl,
|
||||
phone: identity.phone_number || null,
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
idToken: tokens.id_token,
|
||||
|
|
@ -172,6 +174,7 @@ export async function createOrUpdateUser(identity: HackClubIdentity, tokens: OID
|
|||
email: sql`COALESCE(${identity.primary_email || null}, ${usersTable.email})`,
|
||||
slackId: identity.slack_id,
|
||||
avatar: sql`COALESCE(${avatarUrl}, ${usersTable.avatar})`,
|
||||
phone: sql`COALESCE(${identity.phone_number || null}, ${usersTable.phone})`,
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
idToken: tokens.id_token,
|
||||
|
|
|
|||
47
backend/src/lib/sync-phones.ts
Normal file
47
backend/src/lib/sync-phones.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { eq } from 'drizzle-orm'
|
||||
import { db } from '../db'
|
||||
import { usersTable } from '../schemas/users'
|
||||
import { fetchUserIdentity } from './auth'
|
||||
|
||||
export async function syncAllPhoneNumbers() {
|
||||
console.log('[SYNC-PHONES] Starting phone number sync...')
|
||||
|
||||
try {
|
||||
const allUsers = await db
|
||||
.select({
|
||||
id: usersTable.id,
|
||||
username: usersTable.username,
|
||||
accessToken: usersTable.accessToken
|
||||
})
|
||||
.from(usersTable)
|
||||
|
||||
let updated = 0
|
||||
let failed = 0
|
||||
|
||||
for (const u of allUsers) {
|
||||
if (!u.accessToken) {
|
||||
continue
|
||||
}
|
||||
|
||||
const identity = await fetchUserIdentity(u.accessToken)
|
||||
if (!identity) {
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
|
||||
const phone = identity.identity.phone_number || null
|
||||
if (phone) {
|
||||
await db
|
||||
.update(usersTable)
|
||||
.set({ phone, updatedAt: new Date() })
|
||||
.where(eq(usersTable.id, u.id))
|
||||
updated++
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[SYNC-PHONES] Done. Updated ${updated} of ${allUsers.length} users (${failed} failed)`)
|
||||
return { total: allUsers.length, updated, failed }
|
||||
} catch (err) {
|
||||
console.error('[SYNC-PHONES] Error syncing phone numbers:', err)
|
||||
}
|
||||
}
|
||||
|
|
@ -1511,6 +1511,7 @@ admin.get('/orders', async ({ headers, query, status }) => {
|
|||
notes: shopOrdersTable.notes,
|
||||
isFulfilled: shopOrdersTable.isFulfilled,
|
||||
shippingAddress: shopOrdersTable.shippingAddress,
|
||||
phone: shopOrdersTable.phone,
|
||||
createdAt: shopOrdersTable.createdAt,
|
||||
itemId: shopItemsTable.id,
|
||||
itemName: shopItemsTable.name,
|
||||
|
|
|
|||
|
|
@ -278,6 +278,15 @@ shop.post('/items/:id/purchase', async ({ params, body, headers }) => {
|
|||
|
||||
const totalPrice = item.price * quantity
|
||||
|
||||
// Get user's phone number for the order
|
||||
const userPhone = await db
|
||||
.select({ phone: usersTable.phone })
|
||||
.from(usersTable)
|
||||
.where(eq(usersTable.id, user.id))
|
||||
.limit(1)
|
||||
|
||||
const phone = userPhone[0]?.phone || null
|
||||
|
||||
try {
|
||||
const order = await db.transaction(async (tx) => {
|
||||
// Lock the user row to serialize spend operations and prevent race conditions
|
||||
|
|
@ -317,6 +326,7 @@ shop.post('/items/:id/purchase', async ({ params, body, headers }) => {
|
|||
pricePerItem: item.price,
|
||||
totalPrice,
|
||||
shippingAddress: shippingAddress || null,
|
||||
phone,
|
||||
status: 'pending'
|
||||
})
|
||||
.returning()
|
||||
|
|
@ -402,6 +412,15 @@ shop.post('/items/:id/try-luck', async ({ params, headers }) => {
|
|||
return { error: 'Out of stock' }
|
||||
}
|
||||
|
||||
// Get user's phone number for orders
|
||||
const userPhoneResult = await db
|
||||
.select({ phone: usersTable.phone })
|
||||
.from(usersTable)
|
||||
.where(eq(usersTable.id, user.id))
|
||||
.limit(1)
|
||||
|
||||
const userPhone = userPhoneResult[0]?.phone || null
|
||||
|
||||
try {
|
||||
const result = await db.transaction(async (tx) => {
|
||||
// Lock the user row to serialize spend operations and prevent race conditions
|
||||
|
|
@ -483,6 +502,7 @@ shop.post('/items/:id/try-luck', async ({ params, headers }) => {
|
|||
pricePerItem: rollCost,
|
||||
totalPrice: rollCost,
|
||||
shippingAddress: null,
|
||||
phone: userPhone,
|
||||
status: 'pending',
|
||||
orderType: 'luck_win'
|
||||
})
|
||||
|
|
@ -539,6 +559,7 @@ shop.post('/items/:id/try-luck', async ({ params, headers }) => {
|
|||
pricePerItem: rollCost,
|
||||
totalPrice: rollCost,
|
||||
shippingAddress: null,
|
||||
phone: userPhone,
|
||||
status: 'pending',
|
||||
orderType: 'consolation',
|
||||
notes: `Consolation scrap paper - rolled ${rolled}, needed ${effectiveProbability} or less`
|
||||
|
|
|
|||
|
|
@ -36,6 +36,7 @@ export const shopOrdersTable = pgTable('shop_orders', {
|
|||
status: varchar().notNull().default('pending'),
|
||||
orderType: varchar('order_type').notNull().default('purchase'),
|
||||
shippingAddress: text('shipping_address'),
|
||||
phone: varchar(),
|
||||
notes: text(),
|
||||
isFulfilled: boolean('is_fulfilled').notNull().default(false),
|
||||
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export const usersTable = pgTable('users', {
|
|||
username: varchar(),
|
||||
email: varchar().notNull(),
|
||||
avatar: varchar(),
|
||||
phone: varchar(),
|
||||
|
||||
// OAuth tokens
|
||||
accessToken: text('access_token'),
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
notes: string | null;
|
||||
isFulfilled: boolean;
|
||||
shippingAddress: string | null;
|
||||
phone: string | null;
|
||||
createdAt: string;
|
||||
itemId: number;
|
||||
itemName: string;
|
||||
|
|
@ -276,15 +277,18 @@
|
|||
<div class="mt-2 rounded-lg border border-gray-300 bg-gray-100 p-2">
|
||||
<p class="mb-1 text-xs font-bold text-gray-500">shipping address</p>
|
||||
<p class="text-sm">{formatAddress(shippingAddr)}</p>
|
||||
{#if shippingAddr.phone}
|
||||
<p class="mt-1 text-xs text-gray-500">📞 {shippingAddr.phone}</p>
|
||||
{#if order.phone || shippingAddr.phone}
|
||||
<p class="mt-1 text-xs text-gray-500">phone: {order.phone || shippingAddr.phone}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if order.orderType === 'win'}
|
||||
<div class="mt-2 rounded-lg border border-yellow-300 bg-yellow-100 p-2">
|
||||
<p class="text-xs font-bold text-yellow-700">⚠️ no shipping address provided</p>
|
||||
<p class="text-xs font-bold text-yellow-700">no shipping address provided</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if order.phone && !parseShippingAddress(order.shippingAddress)}
|
||||
<p class="mt-1 text-xs text-gray-500">phone: {order.phone}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue