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:
@@ -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],
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -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',
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user