Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions migrations/20260729154654-add-send-after.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

var dbm;
var type;
var seed;
var fs = require('fs');
var path = require('path');
var Promise;

/**
* We receive the dbmigrate dependency from dbmigrate initially.
* This enables us to not have to rely on NODE_PATH.
*/
exports.setup = function(options, seedLink) {
dbm = options.dbmigrate;
type = dbm.dataType;
seed = seedLink;
Promise = options.Promise;
};

exports.up = function(db) {
var filePath = path.join(__dirname, 'sqls', '20260729154654-add-send-after-up.sql');
return new Promise( function( resolve, reject ) {
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
if (err) return reject(err);
resolve(data);
});
})
.then(function(data) {
return db.runSql(data);
});
};

exports.down = function(db) {
var filePath = path.join(__dirname, 'sqls', '20260729154654-add-send-after-down.sql');
return new Promise( function( resolve, reject ) {
fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){
if (err) return reject(err);
resolve(data);
});
})
.then(function(data) {
return db.runSql(data);
});
};

exports._meta = {
"version": 1
};
83 changes: 83 additions & 0 deletions migrations/sqls/20260729154654-add-send-after-down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
drop function sms.send_message;

CREATE OR REPLACE FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code DEFAULT NULL::text, send_before timestamp without time zone DEFAULT NULL::timestamp without time zone) RETURNS sms.outbound_messages
LANGUAGE plpgsql SECURITY DEFINER
AS $$
declare
v_client_id uuid;
v_profile_id uuid;
v_profile_active boolean;
v_contact_zip_code zip_code;
v_estimated_segments integer;
v_result sms.outbound_messages;
begin
select billing.current_client_id() into v_client_id;

if v_client_id is null then
raise 'Not authorized';
end if;

select id, active
from sms.profiles
where client_id = v_client_id
and id = send_message.profile_id
into v_profile_id, v_profile_active;

if v_profile_id is null then
raise 'Profile % not found – it may not exist, or you may not have access', send_message.profile_id using errcode = 'no_data_found';
end if;

if v_profile_active is distinct from true then
raise 'Profile % is inactive', send_message.profile_id;
end if;

if contact_zip_code is null or contact_zip_code = '' then
select sms.map_area_code_to_zip_code(sms.extract_area_code(send_message.to)) into v_contact_zip_code;
else
select contact_zip_code into v_contact_zip_code;
end if;

select sms.estimate_segments(body) into v_estimated_segments;

insert into sms.outbound_messages (profile_id, created_at, to_number, stage, body, media_urls, contact_zip_code, estimated_segments, send_before)
values (send_message.profile_id, date_trunc('second', now()), send_message.to, 'processing', body, media_urls, v_contact_zip_code, v_estimated_segments, send_message.send_before)
returning *
into v_result;

return v_result;
end;
$$;

ALTER FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone) OWNER TO postgres;

GRANT ALL ON FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone) TO client;

CREATE OR REPLACE FUNCTION sms.tg__trigger_process_message() RETURNS trigger
LANGUAGE plpgsql
AS $$
declare
v_channel sms.traffic_channel;
v_job json;
begin
select coalesce(channel, 'grey-route'::sms.traffic_channel)
from sms.profiles
where id = NEW.profile_id
into v_channel;

select row_to_json(NEW) into v_job;

if v_channel = 'grey-route'::sms.traffic_channel then
perform graphile_worker.add_job(identifier => 'process-grey-route-message', payload => v_job, run_at => null, max_attempts => 5);
elsif v_channel = 'toll-free'::sms.traffic_channel then
perform graphile_worker.add_job(identifier => 'process-toll-free-message', payload => v_job, run_at => null, max_attempts => 5);
elsif v_channel = '10dlc'::sms.traffic_channel then
perform graphile_worker.add_job(identifier => 'process-10dlc-message', payload => v_job, run_at => null, max_attempts => 5);
else
raise 'Unsupported traffic channel %', v_channel;
end if;

return NEW;
end;
$$;

alter table sms.outbound_messages drop column send_after;
101 changes: 101 additions & 0 deletions migrations/sqls/20260729154654-add-send-after-up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
alter table sms.outbound_messages add column send_after timestamp;

drop function sms.send_message;

CREATE OR REPLACE FUNCTION sms.send_message(
profile_id uuid,
"to" public.phone_number,
body text,
media_urls public.url[],
contact_zip_code public.zip_code DEFAULT NULL::text,
send_before timestamp without time zone DEFAULT NULL::timestamp without time zone,
send_after timestamp without time zone DEFAULT NULL::timestamp without time zone
) RETURNS sms.outbound_messages
LANGUAGE plpgsql SECURITY DEFINER
AS $$
declare
v_client_id uuid;
v_profile_id uuid;
v_profile_active boolean;
v_contact_zip_code zip_code;
v_estimated_segments integer;
v_result sms.outbound_messages;
begin
select billing.current_client_id() into v_client_id;

if v_client_id is null then
raise 'Not authorized';
end if;

select id, active
from sms.profiles
where client_id = v_client_id
and id = send_message.profile_id
into v_profile_id, v_profile_active;

if v_profile_id is null then
raise 'Profile % not found – it may not exist, or you may not have access', send_message.profile_id using errcode = 'no_data_found';
end if;

if v_profile_active is distinct from true then
raise 'Profile % is inactive', send_message.profile_id;
end if;

if send_message.send_after is not null
and send_message.send_before is not null
and send_message.send_after >= send_message.send_before then
raise 'send_after must be before send_before';
end if;

if contact_zip_code is null or contact_zip_code = '' then
select sms.map_area_code_to_zip_code(sms.extract_area_code(send_message.to)) into v_contact_zip_code;
else
select contact_zip_code into v_contact_zip_code;
end if;

select sms.estimate_segments(body) into v_estimated_segments;

insert into sms.outbound_messages (profile_id, created_at, to_number, stage, body, media_urls, contact_zip_code, estimated_segments, send_before, send_after)
values (send_message.profile_id, date_trunc('second', now()), send_message.to, 'processing', body, media_urls, v_contact_zip_code, v_estimated_segments, send_message.send_before, send_message.send_after)
returning *
into v_result;

return v_result;
end;
$$;

ALTER FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone, send_after timestamp without time zone) OWNER TO postgres;

GRANT ALL ON FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone, send_after timestamp without time zone) TO client;

CREATE OR REPLACE FUNCTION sms.tg__trigger_process_message() RETURNS trigger
LANGUAGE plpgsql
AS $$
declare
v_channel sms.traffic_channel;
v_job json;
begin
select coalesce(channel, 'grey-route'::sms.traffic_channel)
from sms.profiles
where id = NEW.profile_id
into v_channel;

if NEW.send_after is not null and v_channel <> '10dlc'::sms.traffic_channel then
raise 'send_after is only supported for the 10dlc channel';
end if;

select row_to_json(NEW) into v_job;

if v_channel = 'grey-route'::sms.traffic_channel then
perform graphile_worker.add_job(identifier => 'process-grey-route-message', payload => v_job, run_at => null, max_attempts => 5);
elsif v_channel = 'toll-free'::sms.traffic_channel then
perform graphile_worker.add_job(identifier => 'process-toll-free-message', payload => v_job, run_at => null, max_attempts => 5);
elsif v_channel = '10dlc'::sms.traffic_channel then
perform graphile_worker.add_job(identifier => 'process-10dlc-message', payload => v_job, run_at => null, max_attempts => 5);
else
raise 'Unsupported traffic channel %', v_channel;
end if;

return NEW;
end;
$$;
27 changes: 19 additions & 8 deletions schema-dump.sql
Original file line number Diff line number Diff line change
Expand Up @@ -1889,7 +1889,8 @@ CREATE TABLE sms.outbound_messages (
media_urls public.url[],
estimated_segments integer DEFAULT 1,
profile_id uuid,
send_before timestamp without time zone
send_before timestamp without time zone,
send_after timestamp without time zone
)
WITH (autovacuum_vacuum_threshold='50000', autovacuum_vacuum_scale_factor='0', autovacuum_vacuum_cost_limit='1000', autovacuum_vacuum_cost_delay='0');

Expand Down Expand Up @@ -2369,10 +2370,10 @@ COMMENT ON FUNCTION sms.sell_cordoned_numbers(n_days integer) IS '@omit';


--
-- Name: send_message(uuid, public.phone_number, text, public.url[], public.zip_code, timestamp without time zone); Type: FUNCTION; Schema: sms; Owner: postgres
-- Name: send_message(uuid, public.phone_number, text, public.url[], public.zip_code, timestamp without time zone, timestamp without time zone); Type: FUNCTION; Schema: sms; Owner: postgres
--

CREATE FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code DEFAULT NULL::text, send_before timestamp without time zone DEFAULT NULL::timestamp without time zone) RETURNS sms.outbound_messages
CREATE FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code DEFAULT NULL::text, send_before timestamp without time zone DEFAULT NULL::timestamp without time zone, send_after timestamp without time zone DEFAULT NULL::timestamp without time zone) RETURNS sms.outbound_messages
LANGUAGE plpgsql SECURITY DEFINER
AS $$
declare
Expand Down Expand Up @@ -2403,6 +2404,12 @@ begin
raise 'Profile % is inactive', send_message.profile_id;
end if;

if send_message.send_after is not null
and send_message.send_before is not null
and send_message.send_after >= send_message.send_before then
raise 'send_after must be before send_before';
end if;

if contact_zip_code is null or contact_zip_code = '' then
select sms.map_area_code_to_zip_code(sms.extract_area_code(send_message.to)) into v_contact_zip_code;
else
Expand All @@ -2411,8 +2418,8 @@ begin

select sms.estimate_segments(body) into v_estimated_segments;

insert into sms.outbound_messages (profile_id, created_at, to_number, stage, body, media_urls, contact_zip_code, estimated_segments, send_before)
values (send_message.profile_id, date_trunc('second', now()), send_message.to, 'processing', body, media_urls, v_contact_zip_code, v_estimated_segments, send_message.send_before)
insert into sms.outbound_messages (profile_id, created_at, to_number, stage, body, media_urls, contact_zip_code, estimated_segments, send_before, send_after)
values (send_message.profile_id, date_trunc('second', now()), send_message.to, 'processing', body, media_urls, v_contact_zip_code, v_estimated_segments, send_message.send_before, send_message.send_after)
returning *
into v_result;

Expand All @@ -2421,7 +2428,7 @@ end;
$$;


ALTER FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone) OWNER TO postgres;
ALTER FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone, send_after timestamp without time zone) OWNER TO postgres;

--
-- Name: sending_locations; Type: TABLE; Schema: sms; Owner: postgres
Expand Down Expand Up @@ -2845,6 +2852,10 @@ begin
where id = NEW.profile_id
into v_channel;

if NEW.send_after is not null and v_channel <> '10dlc'::sms.traffic_channel then
raise 'send_after is only supported for the 10dlc channel';
end if;

select row_to_json(NEW) into v_job;

if v_channel = 'grey-route'::sms.traffic_channel then
Expand Down Expand Up @@ -5556,10 +5567,10 @@ GRANT SELECT,INSERT,UPDATE ON TABLE lookup.requests TO client;


--
-- Name: FUNCTION send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone); Type: ACL; Schema: sms; Owner: postgres
-- Name: FUNCTION send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone, send_after timestamp without time zone); Type: ACL; Schema: sms; Owner: postgres
--

GRANT ALL ON FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone) TO client;
GRANT ALL ON FUNCTION sms.send_message(profile_id uuid, "to" public.phone_number, body text, media_urls public.url[], contact_zip_code public.zip_code, send_before timestamp without time zone, send_after timestamp without time zone) TO client;


--
Expand Down
45 changes: 45 additions & 0 deletions src/jobs/process-10dlc-message.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,49 @@ describe('process message', () => {
expect(m.sending_location_id).not.toBeNull();
expect(m.from_number).toBe(fromNumber);
});

test('it should carry a requested send_after through to outbound_messages_routing', async () => {
const fromNumber = fakeNumber('877');
const toNumber = fakeNumber();
const sendAfter = new Date(Date.now() + 60 * 60 * 1000);

const m = await withClient(pool, async (client) => {
const { profileId } = await setUpProcessMessage(client, fromNumber);

const {
rows: [message],
} = await client.query(
'select id from sms.send_message($1, $2, $3, $4, $5, $6, $7)',
[
profileId,
toNumber,
faker.hacker.phrase(),
null,
'11238',
null,
sendAfter,
]
);

const foundProcessMessageJob = await findJob<ProcessMessagePayload>(
client,
PROCESS_10DLC_MESSAGE_IDENTIFIER,
'id',
message.id
);

await process10DlcMessage(client, foundProcessMessageJob.payload);

const {
rows: [result],
} = await client.query<outbound_messages_routing>(
`select * from sms.outbound_messages_routing where id = $1`,
[message.id]
);

return result;
});

expect(m.send_after?.getTime()).toBe(sendAfter.getTime());
});
});
4 changes: 2 additions & 2 deletions src/jobs/process-10dlc-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export const process10DlcMessage: WrappableTask = async (
stage: outbound_message_stages.Queued,
from_number: prevMappingRecord.from_number,
sending_location_id: prevMappingRecord.sending_location_id,
send_after: null,
send_after: payload.send_after as unknown as Date | null,
first_from_to_pair_of_day: firstFromToPairOfDay,
});
} else {
Expand Down Expand Up @@ -94,7 +94,7 @@ export const process10DlcMessage: WrappableTask = async (
stage: outbound_message_stages.Queued,
from_number: fromNumber,
sending_location_id: sendingLocationId,
send_after: null,
send_after: payload.send_after as unknown as Date | null,
first_from_to_pair_of_day: true,
});
}
Expand Down
Loading
Loading