Add default automation flows for tenants

Features:
- Auto-provision 5 default email flows for each new tenant:
  - Appointment Confirmation (on event created)
  - Appointment Reminder (X hours before, per service settings)
  - Thank You Email (on final payment)
  - Deposit Payment Confirmation
  - Final Payment Confirmation

- New SmoothSchedule piece triggers:
  - payment_received: Polls for new payments (deposit/final)
  - upcoming_events: Polls for events starting within X hours

- New SmoothSchedule piece action:
  - list_customers: List customers with search, pagination

- Backend APIs:
  - GET /api/v1/payments/ for payment trigger polling
  - GET /api/v1/events/upcoming/ for reminder trigger

- Restore functionality:
  - GET /api/activepieces/default-flows/ to list default flows
  - POST /api/activepieces/default-flows/{type}/restore/ to restore one
  - POST /api/activepieces/default-flows/restore-all/ to restore all

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-21 16:34:29 -05:00
parent 8564b1deba
commit 99f8271003
15 changed files with 1804 additions and 3 deletions

View File

@@ -5,9 +5,10 @@ import { createEventAction, findEventsAction, updateEventAction, cancelEventActi
import { listResourcesAction } from './lib/actions/list-resources';
import { listServicesAction } from './lib/actions/list-services';
import { listInactiveCustomersAction } from './lib/actions/list-inactive-customers';
import { listCustomersAction } from './lib/actions/list-customers';
import { sendEmailAction } from './lib/actions/send-email';
import { listEmailTemplatesAction } from './lib/actions/list-email-templates';
import { eventCreatedTrigger, eventUpdatedTrigger, eventCancelledTrigger, eventStatusChangedTrigger } from './lib/triggers';
import { eventCreatedTrigger, eventUpdatedTrigger, eventCancelledTrigger, eventStatusChangedTrigger, paymentReceivedTrigger, upcomingEventsTrigger } from './lib/triggers';
import { API_URL } from './lib/common';
/**
@@ -75,6 +76,7 @@ export const smoothSchedule = createPiece({
listResourcesAction,
listServicesAction,
listInactiveCustomersAction,
listCustomersAction,
sendEmailAction,
listEmailTemplatesAction,
createCustomApiCallAction({
@@ -89,5 +91,5 @@ export const smoothSchedule = createPiece({
},
}),
],
triggers: [eventCreatedTrigger, eventUpdatedTrigger, eventCancelledTrigger, eventStatusChangedTrigger],
triggers: [eventCreatedTrigger, eventUpdatedTrigger, eventCancelledTrigger, eventStatusChangedTrigger, paymentReceivedTrigger, upcomingEventsTrigger],
});

View File

@@ -5,3 +5,4 @@ export * from './find-events';
export * from './list-resources';
export * from './list-services';
export * from './list-inactive-customers';
export * from './list-customers';

View File

@@ -0,0 +1,102 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
interface PaginatedResponse<T> {
results: T[];
count: number;
next: string | null;
previous: string | null;
}
interface Customer {
id: number;
email: string;
first_name: string;
last_name: string;
phone: string;
notes: string;
created_at: string;
updated_at: string;
}
export const listCustomersAction = createAction({
auth: smoothScheduleAuth,
name: 'list_customers',
displayName: 'List Customers',
description: 'Get a list of customers from SmoothSchedule. Useful for customer lookup and bulk operations.',
props: {
search: Property.ShortText({
displayName: 'Search',
description: 'Search by name, email, or phone number',
required: false,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'Maximum number of customers to return (default: 50, max: 500)',
required: false,
defaultValue: 50,
}),
offset: Property.Number({
displayName: 'Offset',
description: 'Number of customers to skip (for pagination)',
required: false,
defaultValue: 0,
}),
orderBy: Property.StaticDropdown({
displayName: 'Order By',
description: 'Sort order for results',
required: false,
options: {
options: [
{ label: 'Newest First', value: '-created_at' },
{ label: 'Oldest First', value: 'created_at' },
{ label: 'Name (A-Z)', value: 'first_name' },
{ label: 'Name (Z-A)', value: '-first_name' },
{ label: 'Email (A-Z)', value: 'email' },
{ label: 'Last Updated', value: '-updated_at' },
],
},
defaultValue: '-created_at',
}),
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const props = context.propsValue;
const queryParams: Record<string, string> = {};
if (props.search) {
queryParams['search'] = props.search;
}
// Clamp limit to reasonable range
const limit = Math.min(Math.max(props.limit || 50, 1), 500);
queryParams['limit'] = limit.toString();
if (props.offset && props.offset > 0) {
queryParams['offset'] = props.offset.toString();
}
if (props.orderBy) {
queryParams['ordering'] = props.orderBy;
}
const response = await makeRequest<PaginatedResponse<Customer>>(
auth,
HttpMethod.GET,
'/customers/',
undefined,
queryParams
);
return {
customers: response.results || [],
total_count: response.count || 0,
has_more: response.next !== null,
limit: limit,
offset: props.offset || 0,
};
},
});

View File

@@ -2,3 +2,5 @@ export * from './event-created';
export * from './event-updated';
export * from './event-cancelled';
export * from './event-status-changed';
export * from './payment-received';
export * from './upcoming-events';

View File

@@ -0,0 +1,156 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
const TRIGGER_KEY = 'last_payment_check_timestamp';
interface PaymentData {
id: number;
payment_intent_id: string;
amount: string;
currency: string;
type: 'deposit' | 'final';
status: string;
created_at: string;
completed_at: string;
event: {
id: number;
title: string;
start_time: string;
end_time: string;
status: string;
deposit_amount: string | null;
final_price: string | null;
remaining_balance: string | null;
};
service: {
id: number;
name: string;
price: string;
} | null;
customer: {
id: number;
first_name: string;
last_name: string;
email: string;
phone: string;
} | null;
}
export const paymentReceivedTrigger = createTrigger({
auth: smoothScheduleAuth,
name: 'payment_received',
displayName: 'Payment Received',
description: 'Triggers when a payment is successfully completed in SmoothSchedule.',
props: {
paymentType: Property.StaticDropdown({
displayName: 'Payment Type',
description: 'Only trigger for specific payment types',
required: false,
options: {
options: [
{ label: 'All Payments', value: 'all' },
{ label: 'Deposit Payments', value: 'deposit' },
{ label: 'Final Payments', value: 'final' },
],
},
defaultValue: 'all',
}),
},
type: TriggerStrategy.POLLING,
async onEnable(context) {
// Store current timestamp as starting point
await context.store.put(TRIGGER_KEY, new Date().toISOString());
},
async onDisable(context) {
await context.store.delete(TRIGGER_KEY);
},
async test(context) {
const auth = context.auth as SmoothScheduleAuth;
const { paymentType } = context.propsValue;
const queryParams: Record<string, string> = {
limit: '5',
};
if (paymentType && paymentType !== 'all') {
queryParams['type'] = paymentType;
}
const payments = await makeRequest<PaymentData[]>(
auth,
HttpMethod.GET,
'/payments/',
undefined,
queryParams
);
return payments;
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const { paymentType } = context.propsValue;
const lastCheck = await context.store.get<string>(TRIGGER_KEY) || new Date(0).toISOString();
const queryParams: Record<string, string> = {
'created_at__gt': lastCheck,
limit: '100',
};
if (paymentType && paymentType !== 'all') {
queryParams['type'] = paymentType;
}
const payments = await makeRequest<PaymentData[]>(
auth,
HttpMethod.GET,
'/payments/',
undefined,
queryParams
);
if (payments.length > 0) {
// Update the last check timestamp to the most recent payment
const mostRecent = payments.reduce((latest, p) =>
new Date(p.completed_at) > new Date(latest.completed_at) ? p : latest
);
await context.store.put(TRIGGER_KEY, mostRecent.completed_at);
}
return payments;
},
sampleData: {
id: 12345,
payment_intent_id: 'pi_3QDEr5GvIfP3a7s90bcd1234',
amount: '50.00',
currency: 'usd',
type: 'deposit',
status: 'SUCCEEDED',
created_at: '2024-12-01T10:00:00Z',
completed_at: '2024-12-01T10:00:05Z',
event: {
id: 100,
title: 'Consultation with John Doe',
start_time: '2024-12-15T14:00:00Z',
end_time: '2024-12-15T15:00:00Z',
status: 'SCHEDULED',
deposit_amount: '50.00',
final_price: '200.00',
remaining_balance: '150.00',
},
service: {
id: 1,
name: 'Consultation',
price: '200.00',
},
customer: {
id: 50,
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@example.com',
phone: '+1-555-0100',
},
},
});

View File

@@ -0,0 +1,190 @@
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { smoothScheduleAuth, SmoothScheduleAuth } from '../../index';
import { makeRequest } from '../common';
const TRIGGER_KEY_PREFIX = 'reminder_sent_event_ids';
interface UpcomingEventData {
id: number;
title: string;
start_time: string;
end_time: string;
status: string;
hours_until_start: number;
reminder_hours_before: number;
should_send_reminder: boolean;
service: {
id: number;
name: string;
duration: number;
price: string;
reminder_enabled: boolean;
reminder_hours_before: number;
} | null;
customer: {
id: number;
first_name: string;
last_name: string;
email: string;
phone: string;
} | null;
resources: Array<{
id: number;
name: string;
}>;
notes: string | null;
location: {
id: number;
name: string;
address: string;
} | null;
created_at: string;
}
export const upcomingEventsTrigger = createTrigger({
auth: smoothScheduleAuth,
name: 'upcoming_events',
displayName: 'Upcoming Event (Reminder)',
description: 'Triggers for events starting soon. Use for sending appointment reminders.',
props: {
hoursAhead: Property.Number({
displayName: 'Hours Ahead',
description: 'Trigger for events starting within this many hours (matches service reminder settings)',
required: false,
defaultValue: 24,
}),
onlyIfReminderEnabled: Property.Checkbox({
displayName: 'Only if Reminder Enabled',
description: 'Only trigger for events where the service has reminders enabled',
required: false,
defaultValue: true,
}),
},
type: TriggerStrategy.POLLING,
async onEnable(context) {
// Initialize with empty set of processed event IDs
await context.store.put(TRIGGER_KEY_PREFIX, JSON.stringify([]));
},
async onDisable(context) {
await context.store.delete(TRIGGER_KEY_PREFIX);
},
async test(context) {
const auth = context.auth as SmoothScheduleAuth;
const { hoursAhead } = context.propsValue;
const queryParams: Record<string, string> = {
hours_ahead: String(hoursAhead || 24),
limit: '5',
};
const events = await makeRequest<UpcomingEventData[]>(
auth,
HttpMethod.GET,
'/events/upcoming/',
undefined,
queryParams
);
return events;
},
async run(context) {
const auth = context.auth as SmoothScheduleAuth;
const { hoursAhead, onlyIfReminderEnabled } = context.propsValue;
// Get list of event IDs we've already processed
const processedIdsJson = await context.store.get<string>(TRIGGER_KEY_PREFIX) || '[]';
let processedIds: number[] = [];
try {
processedIds = JSON.parse(processedIdsJson);
} catch {
processedIds = [];
}
const queryParams: Record<string, string> = {
hours_ahead: String(hoursAhead || 24),
limit: '100',
};
const events = await makeRequest<UpcomingEventData[]>(
auth,
HttpMethod.GET,
'/events/upcoming/',
undefined,
queryParams
);
// Filter to only events that should trigger reminders
let filteredEvents = events.filter((event) => {
// Skip if already processed
if (processedIds.includes(event.id)) {
return false;
}
// Check if reminder is appropriate based on service settings
if (!event.should_send_reminder) {
return false;
}
// Check if service has reminders enabled
if (onlyIfReminderEnabled && event.service && !event.service.reminder_enabled) {
return false;
}
return true;
});
// Update the processed IDs list
if (filteredEvents.length > 0) {
const newProcessedIds = [...processedIds, ...filteredEvents.map((e) => e.id)];
// Keep only last 1000 IDs to prevent unbounded growth
const trimmedIds = newProcessedIds.slice(-1000);
await context.store.put(TRIGGER_KEY_PREFIX, JSON.stringify(trimmedIds));
}
// Also clean up old IDs (events that have already passed)
// This runs periodically to keep the list manageable
if (Math.random() < 0.1) { // 10% of runs
const currentIds = events.map((e) => e.id);
const activeProcessedIds = processedIds.filter((id) => currentIds.includes(id));
await context.store.put(TRIGGER_KEY_PREFIX, JSON.stringify(activeProcessedIds));
}
return filteredEvents;
},
sampleData: {
id: 12345,
title: 'Consultation with John Doe',
start_time: '2024-12-15T14:00:00Z',
end_time: '2024-12-15T15:00:00Z',
status: 'SCHEDULED',
hours_until_start: 23.5,
reminder_hours_before: 24,
should_send_reminder: true,
service: {
id: 1,
name: 'Consultation',
duration: 60,
price: '200.00',
reminder_enabled: true,
reminder_hours_before: 24,
},
customer: {
id: 50,
first_name: 'John',
last_name: 'Doe',
email: 'john.doe@example.com',
phone: '+1-555-0100',
},
resources: [
{ id: 1, name: 'Dr. Smith' },
],
notes: 'First-time client',
location: {
id: 1,
name: 'Main Office',
address: '123 Business St',
},
created_at: '2024-12-01T10:00:00Z',
},
});